Перенос ветки для демо1 из док-ии Vega
@@ -0,0 +1,287 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
|
||||
import { overrideComponents } from './override-components'
|
||||
|
||||
const gitlab = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 380 380"
|
||||
version="1.1"
|
||||
id="svg578"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs567">
|
||||
<style
|
||||
id="style565">.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style>
|
||||
</defs>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 358.48029,150.34002 -0.48818,-1.24758 -47.26334,-123.347555 a 12.313059,12.313059 0 0 0 -4.86375,-5.858196 12.656595,12.656595 0 0 0 -14.46468,0.777477 12.656595,12.656595 0 0 0 -4.19476,6.364459 l -31.9127,97.636595 H 126.06904 L 94.156344,27.028625 a 12.403463,12.403463 0 0 0 -4.194757,-6.38254 12.656595,12.656595 0 0 0 -14.46468,-0.777477 12.421544,12.421544 0 0 0 -4.863748,5.858196 L 23.279412,149.02012 22.80931,150.2677 a 87.764446,87.764446 0 0 0 29.110169,101.43357 l 0.162727,0.12656 0.433941,0.30738 71.997943,53.91709 35.61928,26.95855 21.69702,16.38125 a 14.591246,14.591246 0 0 0 17.64691,0 l 21.69702,-16.38125 35.61927,-26.95855 72.43189,-54.24255 0.1808,-0.14464 a 87.800608,87.800608 0 0 0 29.07401,-101.32509 z"
|
||||
id="path569"
|
||||
style="stroke-width:1.80808" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="m 358.48029,150.34002 -0.48818,-1.24758 a 159.65391,159.65391 0 0 0 -63.55419,28.56775 l -103.80216,78.48897 c 35.34806,26.74157 66.12167,49.97547 66.12167,49.97547 l 72.43188,-54.24255 0.18081,-0.14465 a 87.800608,87.800608 0 0 0 29.11017,-101.39741 z"
|
||||
id="path571"
|
||||
style="stroke-width:1.80808" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 124.51409,306.12463 35.61928,26.95854 21.69702,16.38125 a 14.591246,14.591246 0 0 0 17.64691,0 l 21.69702,-16.38125 35.61927,-26.95854 c 0,0 -30.80977,-23.30622 -66.15783,-49.97547 -35.34806,26.66925 -66.12167,49.97547 -66.12167,49.97547 z"
|
||||
id="path573"
|
||||
style="stroke-width:1.80808" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 86.815519,177.66019 A 159.45502,159.45502 0 0 0 23.279412,149.02012 l -0.470102,1.24758 a 87.764446,87.764446 0 0 0 29.110169,101.43357 l 0.162727,0.12656 0.433941,0.30738 71.997943,53.91709 c 0,0 30.73745,-23.23389 66.12167,-49.97547 z"
|
||||
id="path575"
|
||||
style="stroke-width:1.80808" />
|
||||
</svg>
|
||||
`
|
||||
|
||||
const new_version = process.env?.VITE_NEW_VERSION;
|
||||
console.log({ base: typeof new_version !== 'undefined' ? '/' : '/docs/' })
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "BeeCloud Docs",
|
||||
description: "Документация публичного облака",
|
||||
head: [['link', { rel: 'icon', href: '/favicon.svg' }]],
|
||||
base: typeof new_version !== 'undefined' ? '/' : '/docs/',
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(tabsMarkdownPlugin)
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: overrideComponents(),
|
||||
}
|
||||
},
|
||||
locales: {
|
||||
root: {
|
||||
label: 'Русский',
|
||||
lang: 'ru',
|
||||
}
|
||||
},
|
||||
themeConfig: {
|
||||
logo: '/favicon.svg',
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
locales: {
|
||||
root: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: 'Поиск',
|
||||
buttonAriaLabel: 'Поиск'
|
||||
},
|
||||
modal: {
|
||||
noResultsText: 'Нет результатов для',
|
||||
resetButtonTitle: 'Сбросить',
|
||||
displayDetails: 'Показать расширенный список',
|
||||
footer: {
|
||||
selectText: 'Выбрать',
|
||||
closeText: 'Закрыть',
|
||||
navigateText: 'Перейти',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{
|
||||
text: 'Документация',
|
||||
link: '/guide/',
|
||||
},
|
||||
// {
|
||||
// text: 'Учебники',
|
||||
// link: '/tutorials/',
|
||||
// },
|
||||
{
|
||||
text: 'Terraform',
|
||||
link: '/terraform/',
|
||||
},
|
||||
{
|
||||
text: 'Консоль управления',
|
||||
link: 'https://console.cloud.dfcloud.ru'
|
||||
}
|
||||
],
|
||||
|
||||
// socialLinks: [
|
||||
// { icon: { svg: gitlab }, link: 'https://git.vimpelcom.ru/common/vega/docs' }
|
||||
// ],
|
||||
|
||||
// editLink: {
|
||||
// pattern: 'https://git.vimpelcom.ru/-/ide/project/common/vega/docs/edit/develop/-/src/:path',
|
||||
// text: 'Отредактируйте эту страницу на GitLab'
|
||||
// },
|
||||
|
||||
docFooter: {
|
||||
next: 'Вперед',
|
||||
prev: 'Назад'
|
||||
},
|
||||
|
||||
lastUpdated: {
|
||||
text: 'Обновлена',
|
||||
formatOptions: {
|
||||
dateStyle: 'long',
|
||||
}
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: 'Содержание'
|
||||
},
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
text: 'Облачные вычисления',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор сервиса', link: '/guide/compute/compute-overview.md' },
|
||||
{ text: 'Быстрый старт', link: '/guide/compute/compute-getting-started.md' },
|
||||
{ text: 'Виртуальные серверы', link: '/guide/compute/compute-instructions/compute-servers-create.md' },
|
||||
{ text: 'Управление виртуальными серверами', link: '/guide/compute/compute-instructions/compute-servers-manage.md' },
|
||||
{ text: 'Диски', link: '/guide/compute/compute-instructions/compute-disks.md' },
|
||||
{ text: 'Группы размещения', link: '/guide/compute/compute-instructions/compute-affinity.md' },
|
||||
{ text: 'IP-адрес', link: '/guide/compute/compute-instructions/compute-ip.md' },
|
||||
{ text: 'Квоты и лимиты', link: '/guide/compute/compute-limits.md' },
|
||||
{ text: 'Уровень обслуживания', link: '/guide/compute/compute-ola.md' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Объектное хранилище',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор сервиса', link: '/guide/storage/storage-overview.md' },
|
||||
{
|
||||
text: 'Подключение к хранилищу',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'WinSCP', link: '/guide/storage/storage-instructions/s3-connect/winscp.md' },
|
||||
{ text: 'S3cmd', link: '/guide/storage/storage-instructions/s3-connect/s3cmd.md' },
|
||||
]
|
||||
},
|
||||
{ text: 'Управление хранилищем', link: '/guide/storage/storage-instructions/storage-s3.md' },
|
||||
{ text: 'Квоты и лимиты', link: '/guide/storage/storage-limits.md' },
|
||||
{ text: 'Уровень обслуживания', link: '/guide/storage/storage-ola.md' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'DNS',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Обзор сервиса', link: '/guide/dns/dns-overview.md' },
|
||||
{ text: 'Ресурсные записи', link: '/guide/dns/dns-instructions/dns-create.md' },
|
||||
{ text: 'Квоты и лимиты', link: '/guide/dns/dns-limits.md' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Аккаунт',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Проекты', link: '/guide/admin/projects.md' },
|
||||
{ text: 'Ролевая модель', link: '/guide/admin/roles.md' },
|
||||
{ text: 'Квоты и лимиты', link: '/guide/admin/limits.md' },
|
||||
{ text: 'Регионы', link: '/guide/admin/availability-matrix.md' },
|
||||
{ text: 'SSH ключи', link: '/guide/admin/ssh.md' },
|
||||
{ text: 'Участники проекта', link: '/guide/admin/users.md' },
|
||||
]
|
||||
},
|
||||
],
|
||||
'/tutorials/': [
|
||||
{
|
||||
text: 'Виртуальные серверы UNIX',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Управление дисками', link: '/tutorials/servers-unix/unix-disks.md' },
|
||||
]
|
||||
},
|
||||
|
||||
],
|
||||
'/terraform/': [
|
||||
{
|
||||
text: 'Terraform',
|
||||
items: [
|
||||
{
|
||||
text: 'BeeCloud провайдер', link: '/terraform/providers/beecloud/index.md',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: 'Облачные вычисления',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: 'Источники данных',
|
||||
items: [
|
||||
{ text: 'beecloud_affinity_groups', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_affinity_groups.md' },
|
||||
{ text: 'beecloud_flavors', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_flavors.md' },
|
||||
{ text: 'beecloud_images', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_images.md' },
|
||||
{ text: 'beecloud_regions', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_regions.md' },
|
||||
{ text: 'beecloud_server', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_server.md' },
|
||||
{ text: 'beecloud_servers', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_servers.md' },
|
||||
{ text: 'beecloud_volume', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_volume.md' },
|
||||
{ text: 'beecloud_volumes', link: '/terraform/providers/beecloud/compute/data-sources/beecloud_volumes.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Ресурсы',
|
||||
items: [
|
||||
{ text: 'beecloud_address_ip', link: '/terraform/providers/beecloud/compute/resources/beecloud_address_ip.md' },
|
||||
{ text: 'beecloud_affinity_group', link: '/terraform/providers/beecloud/compute/resources/beecloud_affinity_group.md' },
|
||||
{ text: 'beecloud_server', link: '/terraform/providers/beecloud/compute/resources/beecloud_server.md' },
|
||||
{ text: 'beecloud_volume_bind', link: '/terraform/providers/beecloud/compute/resources/beecloud_volume_bind.md' },
|
||||
{ text: 'beecloud_volume', link: '/terraform/providers/beecloud/compute/resources/beecloud_volume.md' },
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'DNS',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: 'Источники данных',
|
||||
items: [
|
||||
{ text: 'beecloud_dns_records', link: '/terraform/providers/beecloud/dns/data-sources/beecloud_dns_records.md' },
|
||||
{ text: 'beecloud_dns_zones', link: '/terraform/providers/beecloud/dns/data-sources/beecloud_dns_zones.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Ресурсы',
|
||||
items: [
|
||||
{ text: 'beecloud_dns_record', link: '/terraform/providers/beecloud/dns/resources/beecloud_dns_record.md' },
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Null провайдер', link: '/terraform/providers/null/index.md',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: 'Источники данных',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'null_resource', link: '/terraform/providers/null/resources/null_resource.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Ресурсы',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'null_data_source', link: '/terraform/providers/null/data-sources/null_data_source.md' },
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{ text: 'Вопросы и ответы', link: '/terraform/faq.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* A library to add Matomo tracking to vitepress router.
|
||||
*
|
||||
* @remarks
|
||||
* This injects Matomo default script to the page, while handling SSR.
|
||||
* It requires access to Vitepress router to hook into `onAfterRouteChanged` event.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import type { Router } from "vitepress";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_paq?: any[][] // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for plugin parameters
|
||||
* @public
|
||||
*/
|
||||
export interface IParameters {
|
||||
/**
|
||||
* Enable/disable link click tracking, defaults to true
|
||||
* @defaultValue true
|
||||
*/
|
||||
enableLinkTracking?: boolean;
|
||||
|
||||
/**
|
||||
* Remember consent
|
||||
*
|
||||
* @remarks not working right now
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
rememberConsent?: boolean;
|
||||
|
||||
/**
|
||||
* Requires user consent before sending events
|
||||
*
|
||||
* @remarks not working right now
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
requireConsent?: boolean;
|
||||
|
||||
/**
|
||||
* Vitepress router component
|
||||
*/
|
||||
router: Router;
|
||||
|
||||
/**
|
||||
* Matomo numeric site ID of the site you want to track
|
||||
*/
|
||||
siteID: number;
|
||||
|
||||
/**
|
||||
* Name of the js file to call on the matomo server
|
||||
* @defaultValue "piwik.js"
|
||||
*/
|
||||
trackerJsFile?: string;
|
||||
|
||||
/**
|
||||
* Name of the php file to call on the matomo server
|
||||
* @defaultValue "piwik.php"
|
||||
*/
|
||||
trackerPhpFile?: string;
|
||||
|
||||
/**
|
||||
* URL where the piwik.php/piwik.js files can be found
|
||||
*/
|
||||
trackerUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Matomo in your vitepress project.
|
||||
*
|
||||
* @remarks
|
||||
* This is mostly a generalized version of the basic matomo
|
||||
* tracker code you'd insert in a JS page. However, since vuepress is SSR, it
|
||||
* requires some special workarounds to make sure paq object storage happens
|
||||
* correctly.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default function(parameters: IParameters) {
|
||||
const {
|
||||
router,
|
||||
trackerUrl,
|
||||
rememberConsent = false,
|
||||
requireConsent = false,
|
||||
siteID,
|
||||
trackerJsFile = "piwik.js",
|
||||
trackerPhpFile = "piwik.php",
|
||||
enableLinkTracking = true
|
||||
} = parameters;
|
||||
if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined' &&
|
||||
siteID && trackerUrl) {
|
||||
// We're in SSR space here, meaning that we have to explictly attach _paq to
|
||||
// the window in order to store it globally.
|
||||
if (window._paq == undefined) {
|
||||
window._paq = [];
|
||||
}
|
||||
// Create convenience variable here, but don't expect it to last. Use
|
||||
// window._paq elsewhere when needed, including closure scopes.
|
||||
const _paq = window._paq;
|
||||
// If user requests consent checking, do this before we actually track.
|
||||
// Note: this doesn't work at the moment because the user has no way to set
|
||||
// whether consent was given. Oops.
|
||||
if (requireConsent) {
|
||||
_paq.push(['requireConsent']);
|
||||
if (rememberConsent) {
|
||||
_paq.push(['rememberConsentGiven']);
|
||||
}
|
||||
}
|
||||
if (enableLinkTracking) {
|
||||
_paq.push(['enableLinkTracking']);
|
||||
}
|
||||
(function() {
|
||||
let u=trackerUrl;
|
||||
// Make sure URLs end in a slash
|
||||
if (u.length > 0 && !u.endsWith("/")) {
|
||||
u = u.concat("/");
|
||||
}
|
||||
_paq.push(['setTrackerUrl', u+trackerPhpFile]);
|
||||
_paq.push(['setSiteId', siteID]);
|
||||
const g = document.createElement('script');
|
||||
g.type='text/javascript';
|
||||
g.async=true;
|
||||
g.defer=true;
|
||||
g.src=u+trackerJsFile;
|
||||
document.body.insertBefore(g, document.body.firstChild);
|
||||
})();
|
||||
let existingCallback: typeof router.onAfterRouteChanged;
|
||||
if(router.onAfterRouteChanged) {
|
||||
existingCallback = router.onAfterRouteChanged;
|
||||
}
|
||||
router.onAfterRouteChanged = (to) => {
|
||||
if(existingCallback) {
|
||||
existingCallback(to); // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
}
|
||||
window._paq?.push(['setDocumentTitle', document.title]);
|
||||
window._paq?.push(['setCustomUrl', to]);
|
||||
window._paq?.push(['trackPageView']);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export const overrideComponents = () => (
|
||||
[
|
||||
{
|
||||
find: /^.*\/VPSidebar\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomSidebar.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPNavBar\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomNavBar.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPDoc\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomDoc.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPContent\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomContent.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPDocFooter\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomDocFooter.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPNavBarMenuLink\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomNavBarMenuLink.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPFeature\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomFeature.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPButton\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomButton.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPHero\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomHero.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPNavBarSearchButton\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomNavBarSearchButton.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPLocalSearchBox\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/CustomLocalSearchBox.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { normalizeLink } from 'vitepress/dist/client/theme-default/support/utils'
|
||||
import { EXTERNAL_URL_RE } from 'vitepress/dist/client/shared'
|
||||
|
||||
interface Props {
|
||||
tag?: string
|
||||
size?: 'medium' | 'big'
|
||||
theme?: 'brand' | 'alt' | 'sponsor'
|
||||
text: string
|
||||
href?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium',
|
||||
theme: 'brand'
|
||||
})
|
||||
|
||||
const isExternal = computed(
|
||||
() => props.href && EXTERNAL_URL_RE.test(props.href)
|
||||
)
|
||||
|
||||
const component = computed(() => {
|
||||
return props.tag || props.href ? 'a' : 'button'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
class="VPButton CustomButton"
|
||||
:class="[size, theme]"
|
||||
:href="href ? normalizeLink(href) : undefined"
|
||||
:target="isExternal ? '_blank' : undefined"
|
||||
:rel="isExternal ? 'noreferrer' : undefined"
|
||||
>
|
||||
{{ text }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPButton {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPButton:active {
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.VPButton.medium {
|
||||
border-radius: 12px;
|
||||
padding: 0 20px;
|
||||
line-height: 48px;
|
||||
height: 48px;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.VPButton.big {
|
||||
border-radius: 12px;
|
||||
padding: 0 24px;
|
||||
line-height: 46px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.VPButton.brand {
|
||||
border-color: var(--vp-button-brand-border);
|
||||
color: var(--color-button-contained-text-color);
|
||||
background-color: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
.VPButton.brand:hover {
|
||||
border-color: var(--vp-button-brand-hover-border);
|
||||
color: var(--color-button-contained-text-color);
|
||||
background-color: var(--vp-button-brand-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.brand:active {
|
||||
border-color: var(--vp-button-brand-active-border);
|
||||
color: var(--color-button-contained-text-color);
|
||||
background-color: var(--vp-button-brand-active-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt {
|
||||
border-color: var(--vp-button-alt-border);
|
||||
color: var(--vp-button-alt-text);
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.alt:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor {
|
||||
border-color: var(--vp-button-sponsor-border);
|
||||
color: var(--vp-button-sponsor-text);
|
||||
background-color: var(--vp-button-sponsor-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor:hover {
|
||||
border-color: var(--vp-button-sponsor-hover-border);
|
||||
color: var(--vp-button-sponsor-hover-text);
|
||||
background-color: var(--vp-button-sponsor-hover-bg);
|
||||
}
|
||||
|
||||
.VPButton.sponsor:active {
|
||||
border-color: var(--vp-button-sponsor-active-border);
|
||||
color: var(--vp-button-sponsor-active-text);
|
||||
background-color: var(--vp-button-sponsor-active-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import NotFound from 'vitepress/dist/client/theme-default/NotFound.vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import CustomDoc from './CustomDoc.vue'
|
||||
import VPHome from 'vitepress/dist/client/theme-default/components/VPHome.vue'
|
||||
import VPPage from 'vitepress/dist/client/theme-default/components/VPPage.vue'
|
||||
|
||||
const { page, frontmatter } = useData()
|
||||
const { hasSidebar } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPContent CustomContent"
|
||||
id="VPContent"
|
||||
:class="{
|
||||
'has-sidebar': hasSidebar,
|
||||
'is-home': frontmatter.layout === 'home'
|
||||
}"
|
||||
>
|
||||
<slot name="not-found" v-if="page.isNotFound"><NotFound /></slot>
|
||||
|
||||
<VPPage v-else-if="frontmatter.layout === 'page'">
|
||||
<template #page-top><slot name="page-top" /></template>
|
||||
<template #page-bottom><slot name="page-bottom" /></template>
|
||||
</VPPage>
|
||||
|
||||
<VPHome v-else-if="frontmatter.layout === 'home'">
|
||||
<template #home-hero-before><slot name="home-hero-before" /></template>
|
||||
<template #home-hero-info><slot name="home-hero-info" /></template>
|
||||
<template #home-hero-image><slot name="home-hero-image" /></template>
|
||||
<template #home-hero-after><slot name="home-hero-after" /></template>
|
||||
<template #home-features-before><slot name="home-features-before" /></template>
|
||||
<template #home-features-after><slot name="home-features-after" /></template>
|
||||
</VPHome>
|
||||
|
||||
<component
|
||||
v-else-if="frontmatter.layout && frontmatter.layout !== 'doc'"
|
||||
:is="frontmatter.layout"
|
||||
/>
|
||||
|
||||
<CustomDoc v-else>
|
||||
<template #doc-top><slot name="doc-top" /></template>
|
||||
<template #doc-bottom><slot name="doc-bottom" /></template>
|
||||
|
||||
<template #doc-footer-before><slot name="doc-footer-before" /></template>
|
||||
<template #doc-before><slot name="doc-before" /></template>
|
||||
<template #doc-after><slot name="doc-after" /></template>
|
||||
|
||||
<template #aside-top><slot name="aside-top" /></template>
|
||||
<template #aside-outline-before><slot name="aside-outline-before" /></template>
|
||||
<template #aside-outline-after><slot name="aside-outline-after" /></template>
|
||||
<template #aside-ads-before><slot name="aside-ads-before" /></template>
|
||||
<template #aside-ads-after><slot name="aside-ads-after" /></template>
|
||||
<template #aside-bottom><slot name="aside-bottom" /></template>
|
||||
</CustomDoc>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.VPContent {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
margin: var(--vp-layout-top-height, 0px) auto 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.VPContent.is-home {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPContent {
|
||||
padding-top: var(--vp-nav-height);
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
margin: var(--vp-layout-top-height, 0px) 0 0;
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPContent.has-sidebar {
|
||||
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2);
|
||||
// padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vitepress'
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import VPDocAside from 'vitepress/dist/client/theme-default/components/VPDocAside.vue'
|
||||
import VPDocFooter from 'vitepress/dist/client/theme-default/components/VPDocFooter.vue'
|
||||
|
||||
const { theme } = useData()
|
||||
|
||||
const route = useRoute()
|
||||
const { hasSidebar, hasAside, leftAside } = useSidebar()
|
||||
|
||||
const pageName = computed(() =>
|
||||
route.path.replace(/[./]+/g, '_').replace(/_html$/, '')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="VPDoc CustomDoc"
|
||||
:class="{ 'has-sidebar': hasSidebar, 'has-aside': hasAside }"
|
||||
>
|
||||
<slot name="doc-top" />
|
||||
<div class="container">
|
||||
<div v-if="hasAside" class="aside" :class="{'left-aside': leftAside}">
|
||||
<div class="aside-curtain" />
|
||||
<div class="aside-container">
|
||||
<div class="aside-content">
|
||||
<VPDocAside>
|
||||
<template #aside-top><slot name="aside-top" /></template>
|
||||
<template #aside-bottom><slot name="aside-bottom" /></template>
|
||||
<template #aside-outline-before><slot name="aside-outline-before" /></template>
|
||||
<template #aside-outline-after><slot name="aside-outline-after" /></template>
|
||||
<template #aside-ads-before><slot name="aside-ads-before" /></template>
|
||||
<template #aside-ads-after><slot name="aside-ads-after" /></template>
|
||||
</VPDocAside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content-container">
|
||||
<slot name="doc-before" />
|
||||
<main class="main">
|
||||
<Content
|
||||
class="vp-doc"
|
||||
:class="[
|
||||
pageName,
|
||||
theme.externalLinkIcon && 'external-link-icon-enabled'
|
||||
]"
|
||||
/>
|
||||
</main>
|
||||
<VPDocFooter>
|
||||
<template #doc-footer-before><slot name="doc-footer-before" /></template>
|
||||
</VPDocFooter>
|
||||
<slot name="doc-after" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="doc-bottom" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPDoc {
|
||||
padding: 32px 24px 96px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPDoc {
|
||||
padding: 48px 32px 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPDoc {
|
||||
padding: 48px 32px 0;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-sidebar) .container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 992px;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-sidebar) .content {
|
||||
max-width: 752px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPDoc .container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.VPDoc .aside {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPDoc:not(.has-sidebar) .content {
|
||||
max-width: 784px;
|
||||
}
|
||||
|
||||
.VPDoc:not(.has-sidebar) .container {
|
||||
max-width: 1104px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aside {
|
||||
position: relative;
|
||||
display: none;
|
||||
order: 2;
|
||||
flex-grow: 1;
|
||||
padding-left: 32px;
|
||||
width: 100%;
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.left-aside {
|
||||
order: 1;
|
||||
padding-left: unset;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);
|
||||
width: 224px;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.aside-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside-curtain {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
width: 224px;
|
||||
height: 32px;
|
||||
background: linear-gradient(transparent, var(--vp-c-bg) 70%);
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.content {
|
||||
padding: 0 32px 128px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.content {
|
||||
order: 1;
|
||||
margin: 0;
|
||||
min-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.VPDoc.has-aside .content-container {
|
||||
max-width: 990px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useEditLink } from 'vitepress/dist/client/theme-default/composables/edit-link'
|
||||
import { usePrevNext } from 'vitepress/dist/client/theme-default/composables/prev-next'
|
||||
import VPIconEdit from 'vitepress/dist/client/theme-default/components/icons/VPIconEdit.vue'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
import VPDocFooterLastUpdated from 'vitepress/dist/client/theme-default/components/VPDocFooterLastUpdated.vue'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
|
||||
const editLink = useEditLink()
|
||||
const control = usePrevNext()
|
||||
|
||||
const hasEditLink = computed(() => {
|
||||
return theme.value.editLink && frontmatter.value.editLink !== false
|
||||
})
|
||||
const hasLastUpdated = computed(() => {
|
||||
return page.value.lastUpdated && frontmatter.value.lastUpdated !== false
|
||||
})
|
||||
const showFooter = computed(() => {
|
||||
return hasEditLink.value || hasLastUpdated.value || control.value.prev || control.value.next
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="showFooter" class="VPDocFooter CustomDocFooter">
|
||||
<slot name="doc-footer-before" />
|
||||
|
||||
<div v-if="hasEditLink || hasLastUpdated" class="edit-info">
|
||||
<div v-if="hasEditLink" class="edit-link">
|
||||
<VPLink class="edit-link-button" :href="editLink.url" :no-icon="true">
|
||||
<VPIconEdit class="edit-link-icon" aria-label="edit icon"/>
|
||||
{{ editLink.text }}
|
||||
</VPLink>
|
||||
</div>
|
||||
|
||||
<div v-if="hasLastUpdated" class="last-updated">
|
||||
<VPDocFooterLastUpdated />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav v-if="control.prev?.link || control.next?.link" class="prev-next">
|
||||
<div class="pager">
|
||||
<VPLink v-if="control.prev?.link" class="pager-link prev" :href="control.prev.link">
|
||||
<span class="desc" v-html="theme.docFooter?.prev || 'Previous page'"></span>
|
||||
<span class="title" v-html="control.prev.text"></span>
|
||||
</VPLink>
|
||||
</div>
|
||||
<div class="pager">
|
||||
<VPLink v-if="control.next?.link" class="pager-link next" :href="control.next.link">
|
||||
<span class="desc" v-html="theme.docFooter?.next || 'Next page'"></span>
|
||||
<span class="title" v-html="control.next.text"></span>
|
||||
</VPLink>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.VPDocFooter {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.edit-info {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.edit-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-link-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.edit-link-button:hover {
|
||||
color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.edit-link-icon {
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.prev-next {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 24px;
|
||||
display: grid;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prev-next {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-column-gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.pager-link {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 11px 16px 13px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.pager-link:hover {
|
||||
border-color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.pager-link.next {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1a73e8;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import VPImage from 'vitepress/dist/client/theme-default/components/VPImage.vue'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
import VPIconArrowRight from 'vitepress/dist/client/theme-default/components/icons/VPIconArrowRight.vue'
|
||||
|
||||
defineProps<{
|
||||
icon?: DefaultTheme.FeatureIcon
|
||||
title: string
|
||||
details?: string
|
||||
link?: string
|
||||
linkText?: string
|
||||
rel?: string
|
||||
target?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
class="VPFeature CustomFeature"
|
||||
:href="link"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
:no-icon="true"
|
||||
:tag="link ? 'a' : 'div'"
|
||||
>
|
||||
<article class="box">
|
||||
<div v-if="typeof icon === 'object' && icon.wrap" class="icon">
|
||||
<VPImage
|
||||
:image="icon"
|
||||
:alt="icon.alt"
|
||||
:height="icon.height || 48"
|
||||
:width="icon.width || 48"
|
||||
/>
|
||||
</div>
|
||||
<VPImage
|
||||
v-else-if="typeof icon === 'object'"
|
||||
:image="icon"
|
||||
:alt="icon.alt"
|
||||
:height="icon.height || 48"
|
||||
:width="icon.width || 48"
|
||||
/>
|
||||
<div v-else-if="icon" class="icon" v-html="icon"></div>
|
||||
<h2 class="title" v-html="title"></h2>
|
||||
<p v-if="details" class="details" v-html="details"></p>
|
||||
|
||||
<div v-if="linkText" class="link-text">
|
||||
<p class="link-text-value">
|
||||
{{ linkText }} <VPIconArrowRight class="link-text-icon" />
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFeature {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transition: border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPFeature.link:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.box > :deep(.VPImage) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-default-soft);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 24px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
padding-top: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-inactive);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.link-text-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.link-text-icon {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,329 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, inject } from 'vue'
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import CustomButton from './CustomButton.vue'
|
||||
import VPImage from 'vitepress/dist/client/theme-default/components/VPImage.vue'
|
||||
|
||||
export interface HeroAction {
|
||||
theme?: 'brand' | 'alt'
|
||||
text: string
|
||||
link: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
name?: string
|
||||
text?: string
|
||||
tagline?: string
|
||||
image?: DefaultTheme.ThemeableImage
|
||||
actions?: HeroAction[]
|
||||
}>()
|
||||
|
||||
const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPHero" :class="{ 'has-image': image || heroImageSlotExists }">
|
||||
<div class="container">
|
||||
<div class="main">
|
||||
<slot name="home-hero-info">
|
||||
<h1 v-if="name" class="name">
|
||||
<span v-html="name" class="clip"></span>
|
||||
</h1>
|
||||
<p v-if="text" v-html="text" class="text"></p>
|
||||
<p v-if="tagline" v-html="tagline" class="tagline"></p>
|
||||
</slot>
|
||||
|
||||
<div v-if="actions" class="actions">
|
||||
<div v-for="action in actions" :key="action.link" class="action">
|
||||
<CustomButton
|
||||
tag="a"
|
||||
size="medium"
|
||||
:theme="action.theme"
|
||||
:text="action.text"
|
||||
:href="action.link"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="image || heroImageSlotExists" class="image">
|
||||
<div class="image-container">
|
||||
<div class="image-bg" />
|
||||
<slot name="home-hero-image">
|
||||
<VPImage v-if="image" class="image-src" :image="image" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.VPHero {
|
||||
margin-top: calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);
|
||||
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.VPHero {
|
||||
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHero {
|
||||
padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.container {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
order: 2;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.VPHero.has-image .container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHero.has-image .container {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.main {
|
||||
order: 1;
|
||||
width: calc((100% / 3) * 2);
|
||||
}
|
||||
|
||||
.VPHero.has-image .main {
|
||||
max-width: 592px;
|
||||
}
|
||||
}
|
||||
|
||||
.name,
|
||||
.text {
|
||||
max-width: 392px;
|
||||
letter-spacing: -0.4px;
|
||||
line-height: 40px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.VPHero.has-image .name,
|
||||
.VPHero.has-image .text {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--vp-home-hero-name-color);
|
||||
}
|
||||
|
||||
.clip {
|
||||
background: var(--color-gradient-magma);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: var(--vp-home-hero-name-color);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.name,
|
||||
.text {
|
||||
max-width: 576px;
|
||||
line-height: 56px;
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.name,
|
||||
.text {
|
||||
line-height: 64px;
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.VPHero.has-image .name,
|
||||
.VPHero.has-image .text {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tagline {
|
||||
padding-top: 8px;
|
||||
max-width: 392px;
|
||||
line-height: 28px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.VPHero.has-image .tagline {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.tagline {
|
||||
padding-top: 12px;
|
||||
max-width: 576px;
|
||||
line-height: 32px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.tagline {
|
||||
line-height: 36px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.VPHero.has-image .tagline {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -6px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.VPHero.has-image .actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.actions {
|
||||
padding-top: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPHero.has-image .actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.image {
|
||||
order: 1;
|
||||
margin: -76px -24px -48px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.image {
|
||||
margin: -108px -24px -48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.image {
|
||||
flex-grow: 1;
|
||||
order: 2;
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.image-container {
|
||||
width: 392px;
|
||||
height: 392px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*rtl:ignore*/
|
||||
transform: translate(-32px, -32px);
|
||||
}
|
||||
}
|
||||
|
||||
.image-bg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
/*rtl:ignore*/
|
||||
left: 50%;
|
||||
border-radius: 50%;
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
background-image: var(--vp-home-hero-image-background-image);
|
||||
filter: var(--vp-home-hero-image-filter);
|
||||
/*rtl:ignore*/
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.image-bg {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.image-bg {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.image-src) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
/*rtl:ignore*/
|
||||
left: 50%;
|
||||
max-width: 192px;
|
||||
max-height: 192px;
|
||||
/*rtl:ignore*/
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:deep(.image-src) {
|
||||
max-width: 256px;
|
||||
max-height: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:deep(.image-src) {
|
||||
max-width: 320px;
|
||||
max-height: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,943 @@
|
||||
<script lang="ts" setup>
|
||||
import localSearchIndex from '@localSearchIndex'
|
||||
import {
|
||||
computedAsync,
|
||||
debouncedWatch,
|
||||
onKeyStroke,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
useScrollLock,
|
||||
useSessionStorage
|
||||
} from '@vueuse/core'
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import Mark from 'mark.js/src/vanilla.js'
|
||||
import MiniSearch, { type SearchResult } from 'minisearch'
|
||||
import { dataSymbol, inBrowser, useRouter } from 'vitepress'
|
||||
import {
|
||||
computed,
|
||||
createApp,
|
||||
markRaw,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
watchEffect,
|
||||
type Ref
|
||||
} from 'vue'
|
||||
import type { ModalTranslations } from 'vitepress/types/local-search'
|
||||
import { pathToFile } from 'vitepress/dist/client/app/utils'
|
||||
import { escapeRegExp } from 'vitepress/dist/client/shared'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { LRUCache } from 'vitepress/dist/client/theme-default/support/lru'
|
||||
import { createSearchTranslate } from 'vitepress/dist/client/theme-default/support/translation'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const el = shallowRef<HTMLElement>()
|
||||
const resultsEl = shallowRef<HTMLElement>()
|
||||
|
||||
/* Search */
|
||||
|
||||
const searchIndexData = shallowRef(localSearchIndex)
|
||||
|
||||
// hmr
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept('/@localSearchIndex', (m) => {
|
||||
if (m) {
|
||||
searchIndexData.value = m.default
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface Result {
|
||||
title: string
|
||||
titles: string[]
|
||||
text?: string
|
||||
}
|
||||
|
||||
const vitePressData = useData()
|
||||
const { activate } = useFocusTrap(el, {
|
||||
immediate: true,
|
||||
allowOutsideClick: true,
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: true
|
||||
})
|
||||
const { localeIndex, theme } = vitePressData
|
||||
const searchIndex = computedAsync(async () =>
|
||||
markRaw(
|
||||
MiniSearch.loadJSON<Result>(
|
||||
(await searchIndexData.value[localeIndex.value]?.())?.default,
|
||||
{
|
||||
fields: ['title', 'titles', 'text'],
|
||||
storeFields: ['title', 'titles'],
|
||||
searchOptions: {
|
||||
fuzzy: 0.2,
|
||||
prefix: true,
|
||||
boost: { title: 4, text: 2, titles: 1 },
|
||||
...(theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.miniSearch?.searchOptions)
|
||||
},
|
||||
...(theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.miniSearch?.options)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const disableQueryPersistence = computed(() => {
|
||||
return (
|
||||
theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.disableQueryPersistence === true
|
||||
)
|
||||
})
|
||||
|
||||
const filterText = disableQueryPersistence.value
|
||||
? ref('')
|
||||
: useSessionStorage('vitepress:local-search-filter', '')
|
||||
|
||||
const showDetailedList = useLocalStorage(
|
||||
'vitepress:local-search-detailed-list',
|
||||
theme.value.search?.provider === 'local' &&
|
||||
theme.value.search.options?.detailedView === true
|
||||
)
|
||||
|
||||
const disableDetailedView = computed(() => {
|
||||
return (
|
||||
theme.value.search?.provider === 'local' &&
|
||||
(theme.value.search.options?.disableDetailedView === true ||
|
||||
theme.value.search.options?.detailedView === false)
|
||||
)
|
||||
})
|
||||
|
||||
const buttonText = computed(() => {
|
||||
const options = theme.value.search?.options ?? theme.value.algolia
|
||||
|
||||
return (
|
||||
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
|
||||
options?.translations?.button?.buttonText ||
|
||||
'Search'
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (disableDetailedView.value) {
|
||||
showDetailedList.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const results: Ref<(SearchResult & Result)[]> = shallowRef([])
|
||||
|
||||
const enableNoResults = ref(false)
|
||||
|
||||
watch(filterText, () => {
|
||||
enableNoResults.value = false
|
||||
})
|
||||
|
||||
const mark = computedAsync(async () => {
|
||||
if (!resultsEl.value) return
|
||||
return markRaw(new Mark(resultsEl.value))
|
||||
}, null)
|
||||
|
||||
const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
|
||||
|
||||
debouncedWatch(
|
||||
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
|
||||
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
|
||||
if (old?.[0] !== index) {
|
||||
// in case of hmr
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
let canceled = false
|
||||
onCleanup(() => {
|
||||
canceled = true
|
||||
})
|
||||
|
||||
if (!index) return
|
||||
|
||||
// Search
|
||||
results.value = index
|
||||
.search(filterTextValue)
|
||||
.slice(0, 16) as (SearchResult & Result)[]
|
||||
enableNoResults.value = true
|
||||
|
||||
// Highlighting
|
||||
const mods = showDetailedListValue
|
||||
? await Promise.all(results.value.map((r) => fetchExcerpt(r.id)))
|
||||
: []
|
||||
if (canceled) return
|
||||
for (const { id, mod } of mods) {
|
||||
const mapId = id.slice(0, id.indexOf('#'))
|
||||
let map = cache.get(mapId)
|
||||
if (map) continue
|
||||
map = new Map()
|
||||
cache.set(mapId, map)
|
||||
const comp = mod.default ?? mod
|
||||
if (comp?.render || comp?.setup) {
|
||||
const app = createApp(comp)
|
||||
// Silence warnings about missing components
|
||||
app.config.warnHandler = () => {}
|
||||
app.provide(dataSymbol, vitePressData)
|
||||
Object.defineProperties(app.config.globalProperties, {
|
||||
$frontmatter: {
|
||||
get() {
|
||||
return vitePressData.frontmatter.value
|
||||
}
|
||||
},
|
||||
$params: {
|
||||
get() {
|
||||
return vitePressData.page.value.params
|
||||
}
|
||||
}
|
||||
})
|
||||
const div = document.createElement('div')
|
||||
app.mount(div)
|
||||
const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||
headings.forEach((el) => {
|
||||
const href = el.querySelector('a')?.getAttribute('href')
|
||||
const anchor = href?.startsWith('#') && href.slice(1)
|
||||
if (!anchor) return
|
||||
let html = ''
|
||||
while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName))
|
||||
html += el.outerHTML
|
||||
map!.set(anchor, html)
|
||||
})
|
||||
app.unmount()
|
||||
}
|
||||
if (canceled) return
|
||||
}
|
||||
|
||||
const terms = new Set<string>()
|
||||
|
||||
results.value = results.value.map((r) => {
|
||||
const [id, anchor] = r.id.split('#')
|
||||
const map = cache.get(id)
|
||||
const text = map?.get(anchor) ?? ''
|
||||
for (const term in r.match) {
|
||||
terms.add(term)
|
||||
}
|
||||
return { ...r, text }
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
if (canceled) return
|
||||
|
||||
await new Promise((r) => {
|
||||
mark.value?.unmark({
|
||||
done: () => {
|
||||
mark.value?.markRegExp(formMarkRegex(terms), { done: r })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? []
|
||||
for (const excerpt of excerpts) {
|
||||
excerpt
|
||||
.querySelector('mark[data-markjs="true"]')
|
||||
?.scrollIntoView({ block: 'center' })
|
||||
}
|
||||
// FIXME: without this whole page scrolls to the bottom
|
||||
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
|
||||
},
|
||||
{ debounce: 200, immediate: true }
|
||||
)
|
||||
|
||||
async function fetchExcerpt(id: string) {
|
||||
const file = pathToFile(id.slice(0, id.indexOf('#')))
|
||||
try {
|
||||
if (!file) throw new Error(`Cannot find file for id: ${id}`)
|
||||
return { id, mod: await import(/*@vite-ignore*/ file) }
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return { id, mod: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/* Search input focus */
|
||||
|
||||
const searchInput = ref<HTMLInputElement>()
|
||||
const disableReset = computed(() => {
|
||||
return filterText.value?.length <= 0
|
||||
})
|
||||
function focusSearchInput(select = true) {
|
||||
searchInput.value?.focus()
|
||||
select && searchInput.value?.select()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
focusSearchInput()
|
||||
})
|
||||
|
||||
function onSearchBarClick(event: PointerEvent) {
|
||||
if (event.pointerType === 'mouse') {
|
||||
focusSearchInput()
|
||||
}
|
||||
}
|
||||
|
||||
/* Search keyboard selection */
|
||||
|
||||
const selectedIndex = ref(-1)
|
||||
const disableMouseOver = ref(false)
|
||||
|
||||
watch(results, (r) => {
|
||||
selectedIndex.value = r.length ? 0 : -1
|
||||
scrollToSelectedResult()
|
||||
})
|
||||
|
||||
function scrollToSelectedResult() {
|
||||
nextTick(() => {
|
||||
const selectedEl = document.querySelector('.result.selected')
|
||||
if (selectedEl) {
|
||||
selectedEl.scrollIntoView({
|
||||
block: 'nearest'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onKeyStroke('ArrowUp', (event) => {
|
||||
event.preventDefault()
|
||||
selectedIndex.value--
|
||||
if (selectedIndex.value < 0) {
|
||||
selectedIndex.value = results.value.length - 1
|
||||
}
|
||||
disableMouseOver.value = true
|
||||
scrollToSelectedResult()
|
||||
})
|
||||
|
||||
onKeyStroke('ArrowDown', (event) => {
|
||||
event.preventDefault()
|
||||
selectedIndex.value++
|
||||
if (selectedIndex.value >= results.value.length) {
|
||||
selectedIndex.value = 0
|
||||
}
|
||||
disableMouseOver.value = true
|
||||
scrollToSelectedResult()
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
onKeyStroke('Enter', (e) => {
|
||||
if (e.isComposing) return
|
||||
|
||||
if (e.target instanceof HTMLButtonElement && e.target.type !== 'submit')
|
||||
return
|
||||
|
||||
const selectedPackage = results.value[selectedIndex.value]
|
||||
if (e.target instanceof HTMLInputElement && !selectedPackage) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedPackage) {
|
||||
router.go(selectedPackage.id)
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
emit('close')
|
||||
})
|
||||
|
||||
// Translations
|
||||
const defaultTranslations: { modal: ModalTranslations } = {
|
||||
modal: {
|
||||
displayDetails: 'Display detailed list',
|
||||
resetButtonTitle: 'Reset search',
|
||||
backButtonTitle: 'Close search',
|
||||
noResultsText: 'No results for',
|
||||
footer: {
|
||||
selectText: 'to select',
|
||||
selectKeyAriaLabel: 'enter',
|
||||
navigateText: 'to navigate',
|
||||
navigateUpKeyAriaLabel: 'up arrow',
|
||||
navigateDownKeyAriaLabel: 'down arrow',
|
||||
closeText: 'to close',
|
||||
closeKeyAriaLabel: 'escape'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const $t = createSearchTranslate(defaultTranslations)
|
||||
|
||||
// Back
|
||||
|
||||
onMounted(() => {
|
||||
// Prevents going to previous site
|
||||
window.history.pushState(null, '', null)
|
||||
})
|
||||
|
||||
useEventListener('popstate', (event) => {
|
||||
event.preventDefault()
|
||||
emit('close')
|
||||
})
|
||||
|
||||
/** Lock body */
|
||||
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
isLocked.value = true
|
||||
nextTick().then(() => activate())
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isLocked.value = false
|
||||
})
|
||||
|
||||
function resetSearch() {
|
||||
filterText.value = ''
|
||||
nextTick().then(() => focusSearchInput(false))
|
||||
}
|
||||
|
||||
function formMarkRegex(terms: Set<string>) {
|
||||
return new RegExp(
|
||||
[...terms]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.map((term) => `(${escapeRegExp(term)})`)
|
||||
.join('|'),
|
||||
'gi'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
ref="el"
|
||||
role="button"
|
||||
:aria-owns="results?.length ? 'localsearch-list' : undefined"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="localsearch-label"
|
||||
class="VPLocalSearchBox CustomLocalSearchBox"
|
||||
>
|
||||
<div class="backdrop" @click="$emit('close')" />
|
||||
|
||||
<div class="shell">
|
||||
<form
|
||||
class="search-bar"
|
||||
@pointerup="onSearchBarClick($event)"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<label
|
||||
:title="buttonText"
|
||||
id="localsearch-label"
|
||||
for="localsearch-input"
|
||||
>
|
||||
<svg
|
||||
class="search-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21l-4.35-4.35" />
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
<div class="search-actions before">
|
||||
<button
|
||||
class="back-button"
|
||||
:title="$t('modal.backButtonTitle')"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 12H5m7 7l-7-7l7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="filterText"
|
||||
:placeholder="buttonText"
|
||||
id="localsearch-input"
|
||||
aria-labelledby="localsearch-label"
|
||||
class="search-input"
|
||||
/>
|
||||
<div class="search-actions">
|
||||
<button
|
||||
v-if="!disableDetailedView"
|
||||
class="toggle-layout-button"
|
||||
type="button"
|
||||
:class="{ 'detailed-list': showDetailedList }"
|
||||
:title="$t('modal.displayDetails')"
|
||||
@click="
|
||||
selectedIndex > -1 && (showDetailedList = !showDetailedList)
|
||||
"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 14h7v7H3zM3 3h7v7H3zm11 1h7m-7 5h7m-7 6h7m-7 5h7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="clear-button"
|
||||
type="reset"
|
||||
:disabled="disableReset"
|
||||
:title="$t('modal.resetButtonTitle')"
|
||||
@click="resetSearch"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 5H9l-7 7l7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-2 4l-6 6m0-6l6 6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul
|
||||
ref="resultsEl"
|
||||
:id="results?.length ? 'localsearch-list' : undefined"
|
||||
:role="results?.length ? 'listbox' : undefined"
|
||||
:aria-labelledby="results?.length ? 'localsearch-label' : undefined"
|
||||
class="results"
|
||||
@mousemove="disableMouseOver = false"
|
||||
>
|
||||
<li
|
||||
v-for="(p, index) in results"
|
||||
:key="p.id"
|
||||
role="option"
|
||||
:aria-selected="selectedIndex === index ? 'true' : 'false'"
|
||||
>
|
||||
<a
|
||||
:href="p.id"
|
||||
class="result"
|
||||
:class="{
|
||||
selected: selectedIndex === index
|
||||
}"
|
||||
:aria-label="[...p.titles, p.title].join(' > ')"
|
||||
@mouseenter="!disableMouseOver && (selectedIndex = index)"
|
||||
@focusin="selectedIndex = index"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<div>
|
||||
<div class="titles">
|
||||
<span class="title-icon">#</span>
|
||||
<span
|
||||
v-for="(t, index) in p.titles"
|
||||
:key="index"
|
||||
class="title"
|
||||
>
|
||||
<span class="text" v-html="t" />
|
||||
<svg width="18" height="18" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 18l6-6l-6-6"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="title main">
|
||||
<span class="text" v-html="p.title" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showDetailedList" class="excerpt-wrapper">
|
||||
<div v-if="p.text" class="excerpt" inert>
|
||||
<div class="vp-doc" v-html="p.text" />
|
||||
</div>
|
||||
<div class="excerpt-gradient-bottom" />
|
||||
<div class="excerpt-gradient-top" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-if="filterText && !results.length && enableNoResults"
|
||||
class="no-results"
|
||||
>
|
||||
{{ $t('modal.noResultsText') }} "<strong>{{ filterText }}</strong
|
||||
>"
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="search-keyboard-shortcuts">
|
||||
<span>
|
||||
<kbd :aria-label="$t('modal.footer.navigateUpKeyAriaLabel')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 19V5m-7 7l7-7l7 7"
|
||||
/>
|
||||
</svg>
|
||||
</kbd>
|
||||
<kbd :aria-label="$t('modal.footer.navigateDownKeyAriaLabel')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 5v14m7-7l-7 7l-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</kbd>
|
||||
{{ $t('modal.footer.navigateText') }}
|
||||
</span>
|
||||
<span>
|
||||
<kbd :aria-label="$t('modal.footer.selectKeyAriaLabel')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentcolor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m9 10l-5 5l5 5" />
|
||||
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
|
||||
</g>
|
||||
</svg>
|
||||
</kbd>
|
||||
{{ $t('modal.footer.selectText') }}
|
||||
</span>
|
||||
<span>
|
||||
<kbd :aria-label="$t('modal.footer.closeKeyAriaLabel')">esc</kbd>
|
||||
{{ $t('modal.footer.closeText') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPLocalSearchBox {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--vp-backdrop-bg-color);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.shell {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
margin: 64px auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
background: var(--vp-local-search-bg);
|
||||
width: min(100vw - 60px, 900px);
|
||||
height: min-content;
|
||||
max-height: min(100vh - 128px, 900px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.shell {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-bar {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar:focus-within {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 6px 12px;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-input {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (any-pointer: coarse) {
|
||||
.search-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.search-actions.before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions button {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.search-actions button:not([disabled]):hover,
|
||||
.toggle-layout-button.detailed-list {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.search-actions button.clear-button:disabled {
|
||||
opacity: 0.37;
|
||||
}
|
||||
|
||||
.search-keyboard-shortcuts {
|
||||
font-size: 0.8rem;
|
||||
opacity: 75%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.search-keyboard-shortcuts span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-keyboard-shortcuts {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-keyboard-shortcuts kbd {
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
min-width: 24px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border: 1px solid rgba(128, 128, 128, 0.15);
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 12px;
|
||||
transition: none;
|
||||
line-height: 1rem;
|
||||
border: solid 2px var(--vp-local-search-result-border);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.result > div {
|
||||
margin: 12px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.result > div {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.titles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title.main {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
opacity: 0.5;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.title svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.result.selected {
|
||||
--vp-local-search-result-bg: var(--vp-local-search-result-selected-bg);
|
||||
border-color: var(--vp-local-search-result-selected-border);
|
||||
}
|
||||
|
||||
.excerpt-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
opacity: 75%;
|
||||
pointer-events: none;
|
||||
max-height: 140px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.result.selected .excerpt {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.excerpt :deep(*) {
|
||||
font-size: 0.8rem !important;
|
||||
line-height: 130% !important;
|
||||
}
|
||||
|
||||
.titles :deep(mark),
|
||||
.excerpt :deep(mark) {
|
||||
background-color: var(--vp-local-search-highlight-bg);
|
||||
color: var(--vp-local-search-highlight-text);
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.excerpt :deep(.vp-code-group) .tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.excerpt :deep(.vp-code-group) div[class*='language-'] {
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.excerpt-gradient-bottom {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: linear-gradient(transparent, var(--vp-local-search-result-bg));
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.excerpt-gradient-top {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: linear-gradient(var(--vp-local-search-result-bg), transparent);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.result.selected .titles,
|
||||
.result.selected .title-icon {
|
||||
color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,254 @@
|
||||
<script lang="ts" setup>
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
import { ref, watchPostEffect } from 'vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useLocalNav } from 'vitepress/dist/client/theme-default/composables/local-nav'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import VPNavBarAppearance from 'vitepress/dist/client/theme-default/components/VPNavBarAppearance.vue'
|
||||
import VPNavBarExtra from 'vitepress/dist/client/theme-default/components/VPNavBarExtra.vue'
|
||||
import VPNavBarHamburger from 'vitepress/dist/client/theme-default/components/VPNavBarHamburger.vue'
|
||||
import VPNavBarMenu from 'vitepress/dist/client/theme-default/components/VPNavBarMenu.vue'
|
||||
import VPNavBarSearch from 'vitepress/dist/client/theme-default/components/VPNavBarSearch.vue'
|
||||
import VPNavBarSocialLinks from 'vitepress/dist/client/theme-default/components/VPNavBarSocialLinks.vue'
|
||||
import CustomNavBarTitle from './CustomNavBarTitle.vue'
|
||||
import VPNavBarTranslations from 'vitepress/dist/client/theme-default/components/VPNavBarTranslations.vue'
|
||||
|
||||
defineProps<{
|
||||
isScreenOpen: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'toggle-screen'): void
|
||||
}>()
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const { hasSidebar } = useSidebar()
|
||||
const { hasLocalNav } = useLocalNav()
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const classes = ref<Record<string, boolean>>({})
|
||||
|
||||
watchPostEffect(() => {
|
||||
classes.value = {
|
||||
'has-sidebar': hasSidebar.value,
|
||||
'has-local-nav': hasLocalNav.value,
|
||||
top: frontmatter.value.layout === 'home' && y.value === 0,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavBar CustomNavbar" :class="classes">
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<CustomNavBarTitle>
|
||||
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
|
||||
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
|
||||
</CustomNavBarTitle>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content-body">
|
||||
<slot name="nav-bar-content-before" />
|
||||
<VPNavBarSearch class="search" />
|
||||
<VPNavBarMenu class="menu" />
|
||||
<VPNavBarTranslations class="translations" />
|
||||
<VPNavBarAppearance class="appearance" />
|
||||
<VPNavBarSocialLinks class="social-links" />
|
||||
<VPNavBarExtra class="extra" />
|
||||
<slot name="nav-bar-content-after" />
|
||||
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
<div class="divider-line" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.VPNavBar {
|
||||
position: relative;
|
||||
height: var(--vp-nav-height);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.5s;
|
||||
background-color: #ffffff;
|
||||
|
||||
.dark & {
|
||||
background-color: #141414;
|
||||
}
|
||||
}
|
||||
|
||||
.VPNavBar.has-local-nav {
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-local-nav {
|
||||
// background-color: transparent;
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
|
||||
.VPNavBar:not(.has-sidebar):not(.top) {
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 0 8px 0 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.wrapper {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
// max-width: calc(var(--vp-layout-max-width) - 64px);
|
||||
height: var(--vp-nav-height);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container > .title,
|
||||
.container > .content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container :deep(*) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
height: calc(var(--vp-nav-height) - 1px);
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .title {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
padding: 0 32px;
|
||||
// width: var(--vp-sidebar-width);
|
||||
height: var(--vp-nav-height);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPNavBar.has-sidebar .title {
|
||||
padding-left: 24px;
|
||||
// width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-right: 32px;
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPNavBar.has-sidebar .content {
|
||||
// padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
|
||||
// padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
|
||||
}
|
||||
}
|
||||
|
||||
.content-body {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: var(--vp-nav-height);
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar:not(.top) .content-body {
|
||||
position: relative;
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
|
||||
.VPNavBar:not(.has-sidebar):not(.top) .content-body {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.content-body {
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu + .translations::before,
|
||||
.menu + .appearance::before,
|
||||
.menu + .social-links::before,
|
||||
.translations + .appearance::before,
|
||||
.appearance + .social-links::before {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--vp-c-divider);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.menu + .appearance::before,
|
||||
.translations + .appearance::before {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.appearance + .social-links::before {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
transition: background-color 0.5s;
|
||||
background-color: rgba(25, 28, 52, 0.12);
|
||||
|
||||
.dark & {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DefaultTheme } from 'vitepress/theme'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { isActive } from 'vitepress/dist/client/shared'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
defineProps<{
|
||||
item: DefaultTheme.NavItemWithLink
|
||||
}>()
|
||||
|
||||
const { page } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPLink
|
||||
:class="{
|
||||
VPNavBarMenuLink: true,
|
||||
CustomNavBarMenuLink: true,
|
||||
active: isActive(
|
||||
page.relativePath,
|
||||
item.activeMatch || item.link,
|
||||
!!item.activeMatch
|
||||
)
|
||||
}"
|
||||
:href="item.link"
|
||||
:target="item.target"
|
||||
:rel="item.rel"
|
||||
tabindex="0"
|
||||
>
|
||||
<span v-html="item.text"></span>
|
||||
</VPLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarMenuLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
line-height: var(--vp-nav-height);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-inactive);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ButtonTranslations } from 'vitepress/types/local-search'
|
||||
import { createSearchTranslate } from 'vitepress/dist/client/theme-default/support/translation'
|
||||
|
||||
// Button-Translations
|
||||
const defaultTranslations: { button: ButtonTranslations } = {
|
||||
button: {
|
||||
buttonText: 'Search',
|
||||
buttonAriaLabel: 'Search'
|
||||
}
|
||||
}
|
||||
|
||||
const $t = createSearchTranslate(defaultTranslations)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" class="DocSearch DocSearch-Button CustomDocSearch-Button" :aria-label="$t('button.buttonAriaLabel')">
|
||||
<span class="DocSearch-Button-Container">
|
||||
<svg
|
||||
class="DocSearch-Search-Icon"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
aria-label="search icon"
|
||||
>
|
||||
<path
|
||||
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="DocSearch-Button-Placeholder">{{ $t('button.buttonText') }}</span>
|
||||
</span>
|
||||
<span class="DocSearch-Button-Keys">
|
||||
<kbd class="DocSearch-Button-Key"></kbd>
|
||||
<kbd class="DocSearch-Button-Key">K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
[class*='DocSearch'] {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1);
|
||||
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||
--docsearch-text-color: var(--vp-c-text-1);
|
||||
--docsearch-muted-color: var(--vp-c-text-2);
|
||||
--docsearch-searchbox-shadow: none;
|
||||
--docsearch-searchbox-background: transparent;
|
||||
--docsearch-searchbox-focus-background: transparent;
|
||||
--docsearch-key-gradient: transparent;
|
||||
--docsearch-key-shadow: none;
|
||||
--docsearch-modal-background: var(--vp-c-bg-soft);
|
||||
--docsearch-footer-background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.dark [class*='DocSearch'] {
|
||||
--docsearch-modal-shadow: none;
|
||||
--docsearch-footer-shadow: none;
|
||||
--docsearch-logo-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-background: var(--vp-c-default-soft);
|
||||
--docsearch-hit-color: var(--vp-c-text-2);
|
||||
--docsearch-hit-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48px;
|
||||
height: 55px;
|
||||
background: transparent;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.DocSearch-Button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.DocSearch-Button:focus:not(:focus-visible) {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Search-Icon {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
fill: currentColor;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Search-Icon {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Search-Icon {
|
||||
top: 1px;
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Placeholder {
|
||||
display: none;
|
||||
margin-top: 2px;
|
||||
padding: 0 16px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover .DocSearch-Button-Placeholder {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Button-Placeholder {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Keys {
|
||||
/*rtl:ignore*/
|
||||
direction: ltr;
|
||||
display: none;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button .DocSearch-Button-Keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key {
|
||||
display: block;
|
||||
margin: 2px 0 0 0;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
/*rtl:begin:ignore*/
|
||||
border-right: none;
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding-left: 6px;
|
||||
/*rtl:end:ignore*/
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-family: var(--vp-font-family-base);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: color 0.5s, border-color 0.5s;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
|
||||
/*rtl:begin:ignore*/
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
border-left: none;
|
||||
border-radius: 0 4px 4px 0;
|
||||
padding-left: 2px;
|
||||
padding-right: 6px;
|
||||
/*rtl:end:ignore*/
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key:first-child {
|
||||
font-size: 0 !important;
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key:first-child:after {
|
||||
content: 'Ctrl';
|
||||
font-size: 12px;
|
||||
letter-spacing: normal;
|
||||
color: var(--docsearch-muted-color);
|
||||
}
|
||||
|
||||
.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after {
|
||||
content: '\2318';
|
||||
}
|
||||
|
||||
.DocSearch-Button .DocSearch-Button-Key:first-child > * {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useLangs } from 'vitepress/dist/client/theme-default/composables/langs'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import { normalizeLink } from 'vitepress/dist/client/theme-default/support/utils'
|
||||
import VPImage from 'vitepress/dist/client/theme-default/components/VPImage.vue'
|
||||
|
||||
const { site, theme } = useData()
|
||||
const { hasSidebar } = useSidebar()
|
||||
const { currentLang } = useLangs()
|
||||
|
||||
const link = computed(() =>
|
||||
typeof theme.value.logoLink === 'string'
|
||||
? theme.value.logoLink
|
||||
: theme.value.logoLink?.link
|
||||
)
|
||||
|
||||
const rel = computed(() =>
|
||||
typeof theme.value.logoLink === 'string'
|
||||
? undefined
|
||||
: theme.value.logoLink?.rel
|
||||
)
|
||||
|
||||
const target = computed(() =>
|
||||
typeof theme.value.logoLink === 'string'
|
||||
? undefined
|
||||
: theme.value.logoLink?.target
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavBarTitle CustomNavBarTitle" :class="{ 'has-sidebar': hasSidebar }">
|
||||
<a
|
||||
class="title"
|
||||
:href="link ?? normalizeLink(currentLang.link)"
|
||||
:rel="rel"
|
||||
:target="target"
|
||||
>
|
||||
<slot name="nav-bar-title-before" />
|
||||
<VPImage v-if="theme.logo" class="logo" :image="theme.logo" />
|
||||
<template v-if="theme.siteTitle">{{ theme.siteTitle }}</template>
|
||||
<template v-else-if="theme.siteTitle === undefined">{{ site.title }}</template>
|
||||
<slot name="nav-bar-title-after" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid transparent;
|
||||
width: 100%;
|
||||
height: var(--vp-nav-height);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
.VPNavBarTitle.has-sidebar .title {
|
||||
border-bottom-color: var(--vp-c-divider);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
:deep(.logo) {
|
||||
margin-right: 8px;
|
||||
height: var(--vp-nav-logo-height);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script lang="ts" setup>
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { inBrowser } from 'vitepress'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import VPSidebarItem from 'vitepress/dist/client/theme-default/components/VPSidebarItem.vue'
|
||||
|
||||
const { sidebarGroups, hasSidebar } = useSidebar()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
// a11y: focus Nav element when menu has opened
|
||||
const navEl = ref<HTMLElement | null>(null)
|
||||
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
||||
|
||||
watch(
|
||||
[props, navEl],
|
||||
() => {
|
||||
if (props.open) {
|
||||
isLocked.value = true
|
||||
navEl.value?.focus()
|
||||
} else isLocked.value = false
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
v-if="hasSidebar"
|
||||
class="VPSidebar CustomSidebar"
|
||||
:class="{ open }"
|
||||
ref="navEl"
|
||||
@click.stop
|
||||
>
|
||||
<div class="curtain" />
|
||||
|
||||
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
|
||||
<span class="visually-hidden" id="sidebar-aria-label">
|
||||
Sidebar Navigation
|
||||
</span>
|
||||
|
||||
<slot name="sidebar-nav-before" />
|
||||
|
||||
<div v-for="item in sidebarGroups" :key="item.text" class="group">
|
||||
<VPSidebarItem :item="item" :depth="0" />
|
||||
</div>
|
||||
|
||||
<slot name="sidebar-nav-after" />
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.VPSidebar {
|
||||
position: fixed;
|
||||
top: var(--vp-layout-top-height, 0px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-sidebar);
|
||||
padding: 32px 32px 96px 32px;
|
||||
width: 256px;
|
||||
max-width: 320px;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
opacity: 0;
|
||||
box-shadow: var(--vp-c-shadow-3);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
transform: translateX(-100%);
|
||||
transition: opacity 0.5s, transform 0.25s ease;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.VPSidebar.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(0);
|
||||
transition: opacity 0.25s,
|
||||
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.dark .VPSidebar {
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPSidebar {
|
||||
padding-top: var(--vp-nav-height);
|
||||
// width: var(--vp-sidebar-width);
|
||||
// max-width: 100%;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
box-shadow: none;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPSidebar {
|
||||
// padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
|
||||
// width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.curtain {
|
||||
position: sticky;
|
||||
top: -64px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
margin-top: calc(var(--vp-nav-height) * -1);
|
||||
margin-right: -32px;
|
||||
margin-left: -32px;
|
||||
height: var(--vp-nav-height);
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.group {
|
||||
padding-top: 10px;
|
||||
width: calc(var(--vp-sidebar-width) - 64px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import { h } from 'vue'
|
||||
import type { Theme, EnhanceAppContext } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme-without-fonts'
|
||||
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
|
||||
import './scss/style.scss'
|
||||
import matomo from '../matomo';
|
||||
|
||||
const matomoHost = import.meta.env.VITE_MATOMO_HOST;
|
||||
const matomoSiteId = import.meta.env.VITE_MATOMO_SITE_ID;
|
||||
console.log({matomo: typeof matomoHost !== 'undefined' && matomoHost})
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: () => {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||
})
|
||||
},
|
||||
enhanceApp({ app, router }: EnhanceAppContext) {
|
||||
enhanceAppWithTabs(app)
|
||||
if (typeof matomoHost !== 'undefined' && matomoHost) {
|
||||
matomo({
|
||||
router: router,
|
||||
siteID: matomoSiteId,
|
||||
trackerUrl: matomoHost
|
||||
})
|
||||
}
|
||||
}
|
||||
} satisfies Theme
|
||||
@@ -0,0 +1,4 @@
|
||||
@use "vp-doc.scss";
|
||||
@use "vp-custom-block.scss";
|
||||
@use "vp-doc-aside.scss";
|
||||
@use "vp-sidebar.scss";
|
||||
@@ -0,0 +1,11 @@
|
||||
.custom-block {
|
||||
padding: 24px;
|
||||
|
||||
&-title {
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 500;
|
||||
letter-spacing: .2px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.VPDocAside {
|
||||
.outline-link {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
@mixin font_style($fontSize, $fontWeight, $lineHeight, $letterSpacing) {
|
||||
font-size: $fontSize;
|
||||
font-weight: $fontWeight;
|
||||
line-height: $lineHeight;
|
||||
letter-spacing: $letterSpacing;
|
||||
}
|
||||
|
||||
@mixin generate-numbered-list-styles($start, $end) {
|
||||
@for $counter from $start through $end {
|
||||
$counter-name: list + ' ' + ($counter - 1);
|
||||
ol[start*="#{$counter}"] {
|
||||
list-style-type: none;
|
||||
counter-reset: $counter-name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
font-size: 17px;
|
||||
|
||||
// Titles
|
||||
h1 {
|
||||
@include font_style(44px, 500, 46px, 0.3px);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include font_style(34px, 400, 36px, 0.3px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include font_style(26px, 500, 32px, 0.2px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@include font_style(20px, 700, 28px, 0.2px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@include font_style(17px, 500, 22px, 0.2px);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// Text
|
||||
ol {
|
||||
list-style-type: none;
|
||||
counter-reset: list;
|
||||
margin: 0 0 0 50px;
|
||||
padding: 0 0 5px 0;
|
||||
font-size: 16px;
|
||||
|
||||
& > * + * {
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@include generate-numbered-list-styles(2, 50);
|
||||
|
||||
ol li {
|
||||
position: relative;
|
||||
padding: 5px 0 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ol li:nth-last-of-type(n+2)::after {
|
||||
content: '';
|
||||
border-left: 1px solid rgb(201, 197, 197);
|
||||
position: absolute;
|
||||
line-height: 100%;
|
||||
left: -30px;
|
||||
top: 43px;
|
||||
bottom: -30px;
|
||||
}
|
||||
|
||||
ol li::before {
|
||||
content: counter(list);
|
||||
counter-increment: list;
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -48px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: #7e00ed;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
font-size: 16px;
|
||||
border-radius: 50%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul li:nth-last-of-type(n):after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
ol ul li::before {
|
||||
counter-increment: list;
|
||||
content: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol li p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
// Links
|
||||
a {
|
||||
color: #1a73e8;
|
||||
|
||||
&:hover, &:focus-visible, &:focus, &:active {
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: #7e00ed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.VPSidebar {
|
||||
&Item {
|
||||
.text {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
:root {
|
||||
--color-text-active: rgba(9, 11, 22, 0.94);
|
||||
--color-text-inactive: rgba(25, 28, 52, 0.7);
|
||||
--color-gradient-magma: linear-gradient(-46deg, #1a73e8 0%, #bc00b8 49.44%, #f55555 100%);
|
||||
|
||||
// Button
|
||||
--color-button-contained-background-color: #fdd835;
|
||||
--color-button-contained-hover-background-color: #fdc435;
|
||||
--color-button-contained-text-color: rgba(9, 11, 22, 0.94);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-text-active: rgba(255, 255, 255, 0.87);
|
||||
--color-text-inactive: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
$font-path-beeline-sans: '/fonts/beeline-sans' !default;
|
||||
|
||||
@mixin beeline-sans-font($type, $weight, $style: normal) {
|
||||
@font-face {
|
||||
font-family: "Beeline Sans";
|
||||
src:url('#{$font-path-beeline-sans}/BeelineSans-#{$type}.woff2') format('woff2'),
|
||||
url('#{$font-path-beeline-sans}/BeelineSans-#{$type}.woff') format('woff'),
|
||||
url('#{$font-path-beeline-sans}/BeelineSans-#{$type}.ttf') format('truetype');
|
||||
font-weight: $weight;
|
||||
font-style: $style;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin beeline-sans-font-pair($type, $weight) {
|
||||
@include beeline-sans-font($type, $weight);
|
||||
|
||||
}
|
||||
|
||||
@include beeline-sans-font-pair(Regular, 400);
|
||||
|
||||
@include beeline-sans-font-pair(Medium, 500);
|
||||
|
||||
@include beeline-sans-font-pair(Bold, 700);
|
||||
|
||||
@include beeline-sans-font-pair(Black, 900);
|
||||
|
||||
$font-path-roboto-mono: '/fonts/roboto-mono' !default;
|
||||
|
||||
@mixin roboto-mono-font($type, $weight, $style: normal) {
|
||||
@font-face {
|
||||
font-family: "Roboto Mono";
|
||||
src:url('#{$font-path-roboto-mono}/RobotoMono-#{$type}.woff2') format('woff2'),
|
||||
url('#{$font-path-roboto-mono}/RobotoMono-#{$type}.woff') format('woff'),
|
||||
url('#{$font-path-roboto-mono}/RobotoMono-#{$type}.ttf') format('truetype');
|
||||
font-weight: $weight;
|
||||
font-style: $style;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin roboto-mono-font-pair($type, $weight) {
|
||||
@include roboto-mono-font($type, $weight);
|
||||
|
||||
}
|
||||
|
||||
@include roboto-mono-font-pair(Light, 300);
|
||||
|
||||
@include roboto-mono-font-pair(Regular, 400);
|
||||
|
||||
@include roboto-mono-font-pair(Medium, 500);
|
||||
|
||||
@include roboto-mono-font-pair(Bold, 700);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@use "fonts.scss";
|
||||
@use "design-system.scss";
|
||||
@use "vars.scss";
|
||||
@use "components";
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attched to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: #fdc435; // var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: #fdc435; // var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: #fdd835; // var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: #fff7d7; // var(--vp-c-indigo-soft);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
|
||||
--vp-c-warning-1: #ff9419; // var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
--vp-c-divider: rgba(25, 28, 52, 0.12);
|
||||
// --vp-sidebar-width: 272;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-divider: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Colors: Background
|
||||
*
|
||||
* - `bg`: The bg color used for main screen.
|
||||
*
|
||||
* - `bg-alt`: The alternative bg color used in places such as "sidebar",
|
||||
* or "code block".
|
||||
*
|
||||
* - `bg-elv`: The elevated bg color. This is used at parts where it "floats",
|
||||
* such as "dialog".
|
||||
*
|
||||
* - `bg-soft`: The bg color to slightly distinguish some components from
|
||||
* the page. Used for things like "carbon ads" or "table".
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-bg: #ffffff;
|
||||
--vp-c-bg-alt: #f9f9f9;
|
||||
--vp-c-bg-elv: #ffffff;
|
||||
--vp-c-bg-soft: #f9f9f9;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg: #141414;
|
||||
--vp-c-bg-alt: #212121;
|
||||
--vp-c-bg-elv: #2e2f33;
|
||||
--vp-c-bg-soft: #36383c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typography
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-font-family-base: 'Beeline Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
--vp-font-family-mono: 'Roboto Mono', monospace;
|
||||
|
||||
// Code
|
||||
// --vp-code-font-size: ;
|
||||
--vp-code-color: rgba(9, 11, 22, 0.94);
|
||||
--vp-code-link-color: rgba(9, 11, 22, 0.94);
|
||||
--vp-code-link-hover-color: rgba(9, 11, 22, 0.94);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-code-color: rgba(255, 255, 255, 0.87);
|
||||
--vp-code-link-color: rgba(255, 255, 255, 0.87);
|
||||
--vp-code-link-hover-color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#bd34fe 30%,
|
||||
#41d1ff
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#bd34fe 50%,
|
||||
#47caff 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-font-size: 17px;
|
||||
--vp-custom-block-code-font-size: 17px;
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: #f1f1f3;
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-warning-bg: #fff4e1;
|
||||
--vp-custom-block-danger-bg: #ffecef;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-custom-block-warning-bg: #3d392a;
|
||||
--vp-custom-block-tip-bg: #36383c;
|
||||
--vp-custom-block-danger-bg: #371313;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Матрица региональной доступности
|
||||
|
||||
Регион доступности — это один или несколько центров обработки данных (ЦОД), в которых могут быть размещены компоненты облачной инфраструктуры.
|
||||
|
||||
| Регион | Статус | Гипервизор | Процессор | HDD| SSD | NVME|
|
||||
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| **ДатаФорт 1** | Доступен | OpenStack | Intel® Xeon® Gold 6248R | ✘ | ✘ | ✅ |
|
||||
|
||||
Условные обозначения:
|
||||
|
||||
✅ — есть возможность выдачи ресурсов.
|
||||
|
||||
✘ — нет возможности выдачи ресурсов.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Квоты и лимиты
|
||||
|
||||
Ограничения включают в себя лимиты и квоты на потребление ресурсов в проекте.
|
||||
|
||||
Квоты ограничивают потребление ресурсов в проекте. В проекте на каждый ресурс выделяется квота, не превышающая лимит.
|
||||
|
||||
После создания проекту становятся доступны базовые квоты. Для них установлены значения по умолчанию.
|
||||
|
||||
**Базовые квоты**
|
||||
|
||||
| Название квоты | Количество |
|
||||
|---------------------|------------|
|
||||
| Количество виртуальных серверов | 3 штуки|
|
||||
| ЦПУ | 200 |
|
||||
| ОЗУ | 200 Гбайт |
|
||||
| Хранилище NVME | 5000 Гбайт |
|
||||
| Объектное хранилище | 100 Гбайт |
|
||||
|
||||
## Просмотр квот проекта
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. В шапке выберите **Проекты**.
|
||||
3. Откроется список проектов, в которых вы являетесь участником.
|
||||
4. Нажмите на имя нужного проекта.
|
||||
5. Откройте раздел **Обзор**.
|
||||
|
||||
## Редактирование квот проекта
|
||||
|
||||
::: tip Информация
|
||||
Изменить квоты проекта может пользователь с ролью **Владелец проекта**.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. В шапке выберите **Проекты**.
|
||||
3. Откроется список проектов, в которых вы являетесь участником.
|
||||
4. Нажмите на имя нужного проекта.
|
||||
5. Откройте раздел **Обзор**.
|
||||
6. В правом верхнем углу нажмите **Изменить квоты**.
|
||||
7. Увеличите или уменьшите квоты для ресурсов.
|
||||
8. Нажмите **Сохранить**.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Управление проектами
|
||||
|
||||
Проект — это структурная единица публичного облака, в которой содержатся ресурсы: виртуальные машины, хранилища, IP-адреса и др.
|
||||
|
||||
Ресурсы могут быть вычислительными и аппаратными.
|
||||
|
||||
Вычислительные ресурсы:
|
||||
- оперативная память (ОЗУ);
|
||||
- ядра процессора (ЦПУ);
|
||||
- локальные диски;
|
||||
- сетевые диски;
|
||||
- IP-адреса.
|
||||
|
||||
Аппаратные ресурсы (серверы, сети, диски) размещены в центрах обработки данных (ЦОД). Каждый дата-центр разделен на модули. Модули оснащены независимыми системами электропитания и охлаждения.
|
||||
|
||||
При получении доступа в публичное облако текущий пользователь становится менеджера проектов. Менеджер проектов может создавать новые проекты, в которых он получает роль владельца проекта. Владелец проекта может добавлять пользователей в проект, назначая им роли.
|
||||
|
||||
Доступ к проекту осуществляется из консоли управления.
|
||||
|
||||
## Создать проект
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Нажмите кнопку **Создать → Проект** в правом верхнем углу.
|
||||
3. Заполните информацию о проекте:
|
||||
- **Название**: введите наименование проекта.
|
||||
- **Идентификатор**: введите идентификатор проекта:
|
||||
- допустимы строчные и прописные буквы латинского алфавита, цифры и дефис;
|
||||
- длина не более 64 символов;
|
||||
- не должно начинаться или заканчиваться дефисом.
|
||||
- **Описание**: введите краткое описание проекта.
|
||||
4. Нажмите **Создать**.
|
||||
|
||||
## Изменить имя проекта
|
||||
|
||||
::: tip Информация
|
||||
Изменить имя и описание проекта может только пользователь с ролью **Владелец проекта**.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. В шапке выберите **Проекты**.
|
||||
3. Откроется список проектов, в которых вы являетесь участником.
|
||||
4. Нажмите на имя нужного проекта.
|
||||
5. Откройте раздел **Настройки → Основное**.
|
||||
6. Измените имя, описание проекта.
|
||||
7. Нажмите **Сохранить**.
|
||||
|
||||
## Удаление проекта
|
||||
|
||||
Функциональность не предусмотрена в публичном облаке.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Ролевая модель
|
||||
|
||||
Управление проектом основано на ролевой модели.
|
||||
|
||||
**Базовые роли**
|
||||
|
||||
В проекте предусмотрен базовый набор ролей:
|
||||
|
||||
- **Владелец продукта** — управление пользователями проекта, просмотр ресурсов.
|
||||
- **DevOps-инженер** — управление инфраструктурой, стандартное администрирование ОС UNIX по протоколу ssh и права управления виртуальными серверами и дисками в консоли управления.
|
||||
|
||||
## Матрица ролей
|
||||
|
||||
| Действие | Владелец проекта | DevOps-инженер |
|
||||
|---|---|---|
|
||||
| Обзор проекта<br> (квоты и количество использованных ресурсов)| ✅ | ✅ |
|
||||
| Серверы: обзор | ✅ | ✅ |
|
||||
| Серверы: мониторинг | ✅ | ✅ |
|
||||
| Серверы: создать сервер |✘ | ✅ |
|
||||
| Серверы: подключить диск | ✘ | ✅ |
|
||||
| Серверы: отключить диск | ✘ | ✅ |
|
||||
| Серверы: добавить диск | ✘ | ✅ |
|
||||
| Серверы: теги | ✘ |✅ |
|
||||
| Серверы: масштабирование сервера | ✘ | ✅ |
|
||||
| Серверы: выключить сервер | ✘ | ✅ |
|
||||
| Серверы: включить сервер | ✘ | ✅ |
|
||||
| Серверы: перезагрузить сервер | ✘ | ✅ |
|
||||
| Серверы: принудительно перезагрузить сервер | ✘ | ✅ |
|
||||
| Серверы: удалить сервер | ✘ | ✅ |
|
||||
| Серверы: группы размещения | ✘ | ✅ |
|
||||
| Серверы: IP-адреса | ✘ | ✅ |
|
||||
| Диски: просмотр дисков | ✅ | ✅ |
|
||||
| Диски: добавление дискового пространства | ✘ | ✅ |
|
||||
| Диски: удалить диск| ✘ | ✅ |
|
||||
| Объектное хранилище: просмотр| ✅ | ✅ |
|
||||
| Объектное хранилище: добавить хранилище | ✘ | ✅ |
|
||||
| Объектное хранилище: удалить хранилище | ✘ | ✅|
|
||||
| DNS: добавить зону | ✘ | ✅ |
|
||||
| DNS: редактировать зону | ✘ | ✅ |
|
||||
| DNS: удалить зону |✘ | ✅ |
|
||||
| Настройки проекта: просмотр| ✅ | ✅ |
|
||||
| Настройки проекта: изменить описание проекта |✅ | ✘ |
|
||||
| Участники: просмотр | ✅ | ✅ |
|
||||
| Участники: добавить участника | ✅ | ✘ |
|
||||
| Участники: удалить участника | ✅ | ✘ |
|
||||
| Участники: назначить роль | ✅| ✘ |
|
||||
| Квоты: просмотр | ✅ | ✅ |
|
||||
| Веб-обработчики | ✘ | ✅ |
|
||||
| Наблюдаемость | ✅ | ✅ |
|
||||
@@ -0,0 +1,38 @@
|
||||
# SSH-ключи
|
||||
|
||||
SSH-ключи используются для подключения к виртуальной машине по SSH. SSH-ключ состоит из публичного и приватного ключей: публичный ключ хранится в профиле пользователя в публичном облаке, приватный — хранится у пользователя.
|
||||
|
||||
## Создать SSH-ключ
|
||||
|
||||
1. Перейдите в профиль пользователя.
|
||||
2. Перейдите в раздел **SSH-ключи**.
|
||||
3. Нажмите **Добавить ключ**.
|
||||
4. Укажите название ключа.
|
||||
5. Откройте терминал и сгенерируйте ключевую пару. Можно использовать команду:
|
||||
|
||||
```sh
|
||||
ssh-keygen -t ed25519 -C “login” -Z aes256-gcm@openssh.com
|
||||
```
|
||||
6. Добавьте публичную часть ключа в поле **SSH-ключ**. Пример публичной части ключа:
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5ABFLIFyapYheN7OZNhTaNqEHefjmU5mtzK********+gRPCz user@Desktop
|
||||
|
||||
```
|
||||
|
||||
## Изменить название ключа
|
||||
|
||||
1. Перейдите в профиль пользователя.
|
||||
2. Перейдите в раздел **SSH-ключи**.
|
||||
3. Выберите нужный ключ.
|
||||
4. Нажмите на … и выберите **Редактировать**.
|
||||
5. Измените имя ключа.
|
||||
6. Нажмите **Сохранить**.
|
||||
|
||||
## Удалить ключ
|
||||
|
||||
1. Перейдите в профиль пользователя.
|
||||
2. Перейдите в раздел **SSH-ключи**.
|
||||
3. Выберите нужный ключ.
|
||||
4. Нажмите на … и выберите **Удалить**.
|
||||
5. Нажмите **Удалить**, чтобы подтвердить удаление ключа.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Управление пользователями в проекте
|
||||
|
||||
В консоли управления можно добавлять пользователей, управлять ролями пользователей в проекте. Один пользователей может участвовать в нескольких проектах и иметь в них разные роли.
|
||||
|
||||
::: tip Информация
|
||||
Добавлять и удалять пользователей, изменять права пользователей в проекте может только владелец проекта.
|
||||
:::
|
||||
|
||||
## Добавить пользователя
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Настройки → Участники**.
|
||||
3. Нажмите **Добавить пользователя**.
|
||||
4. Найдите пользователя по ФИО или email.
|
||||
5. Назначьте [роль](../admin/roles.md) пользователю.
|
||||
6. Нажмите **Добавить**.
|
||||
|
||||
## Назначить права пользователю
|
||||
|
||||
Каждому пользователю проекта должна быть выдана хотя бы одна роль. У пользователя может быть несколько ролей в одном проекте.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Настройки → Участники**.
|
||||
3. Найдите пользователя.
|
||||
4. Нажмите ⠇ в строке с именем пользователя и выберите **Редактировать**.
|
||||
5. Назначьте [роль](../admin/roles.md) пользователю: установите флажок напротив роли.
|
||||
6. Нажмите **Сохранить**.
|
||||
|
||||
Права на существующие ОС применятся в течение 10 минут.
|
||||
|
||||
## Отозвать права у пользователя
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Настройки → Участники**.
|
||||
3. Найдите пользователя.
|
||||
4. Нажмите ⠇ в строке с именем пользователя и выберите **Редактировать**.
|
||||
5. Отзовите роль у пользователя: уберите флажок напротив роли. Оставьте пользователю хотя бы одну роль в проекте.
|
||||
6. Нажмите кнопку **Сохранить**.
|
||||
|
||||
## Удалить пользователя
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Настройки → Участники**.
|
||||
3. Найдите пользователя.
|
||||
4. Нажмите ⠇ в строке с именем пользователя и выберите **Удалить**.
|
||||
|
||||
Пользователь будет удален из списка участников проекта. Ресурсы проекта станут недоступны пользователю.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Быстрый старт
|
||||
|
||||
В этой инструкции рассмотрен процесс создания виртуального сервера UNIX и подключение к нему по протоколу SSH.
|
||||
|
||||
## Перед началом работы
|
||||
|
||||
- [Создан проект](../admin/projects.md#создать-проект).
|
||||
- Назначена роль **DevOps-инженер**.
|
||||
|
||||
## Создать виртуальный сервер
|
||||
|
||||
1. Откройте [консоль управления](https://console.cloud.dfcloud.ru).
|
||||
2. Выбрать проект.
|
||||
3. На странице **Обзор** убедитесь в наличии свободных ресурсов.
|
||||
4. Нажмите **Создать сервер**.
|
||||
5. Выберите образ ОС UNIX.
|
||||
6. [Заполните свойства cервера](../compute/compute-instructions/compute-servers-create.md#создать-сервер).
|
||||
7. Нажмите **Cоздать сервер**.
|
||||
|
||||
Виртуальный сервер отобразится на странице **Облачные вычисления → Серверы**. Выполняется сборка виртуального сервера. После окончания сборки сервер перейдет в статус `Включен`.
|
||||
|
||||
## Подключиться к серверу по SSH
|
||||
|
||||
Для подключения к виртуальному серверу по SSH выполните команду в терминале:
|
||||
|
||||
- по IP-адресу сервера:
|
||||
|
||||
```
|
||||
ssh -l <логин пользователя> -i <путь до приватного ключа> <IP-адрес сервера>
|
||||
```
|
||||
- по полному доменному имени сервера (FQDN):
|
||||
|
||||
```
|
||||
ssh -l <логин пользователя> -i <путь до приватного ключа> <FQDN сервера>
|
||||
```
|
||||
|
||||
Пример команды подключения к серверу по IP-адресу:
|
||||
|
||||
```
|
||||
$ ssh -l ivanov -i /home/user/.ssh/id_rsa 10.0.0.1
|
||||
```
|
||||
|
||||
## Далее
|
||||
|
||||
- [Подключение дополнительного диска к виртуальному серверу](../compute/compute-instructions/compute-disks.md#добавить-диск).
|
||||
- [Удаление виртуального сервера](../compute/compute-instructions/compute-servers-manage.md#удалить-сервер).
|
||||
@@ -0,0 +1,52 @@
|
||||
# Группы размещения
|
||||
|
||||
Группы размещения — это правила размещения виртуальных серверов на физических хостах. Правила размещения позволяют создавать виртуальные серверы на разных или на одном хосте. Политика размещения серверов действует в рамках одной зоны доступности.
|
||||
|
||||
- Правило `Affinity` размещает серверы обязательно на одном физическом хосте.
|
||||
|
||||
- Правило `Soft-Affinity` размещает серверы по возможности на одном физическом хосте.
|
||||
|
||||
- Правило `Anti-Affinity` размещает серверы обязательно на разных физических хостах. Такое размещение повышает производительность и предотвращает недоступность сервера при отказе хоста сервера.
|
||||
|
||||
- Правило `Soft-Anti-Affinity` размещает серверы по возможности на разных физических хостах.
|
||||
|
||||
::: warning Важно
|
||||
Виртуальный сервер создается в группе размещения. Существующий сервер не может быть добавлен в группу размещения.
|
||||
|
||||
Виртуальный сервер может быть создан в группе размещения, если для выполнения правила есть ресурсы в зоне доступности. Если ресурсов нет, то виртуальный сервер не будет создан.
|
||||
:::
|
||||
|
||||
## Создать группу размещения
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Группы размещения**.
|
||||
3. Нажмите **Создать группу**.
|
||||
4. Введите параметры группы размещения:
|
||||
- **Имя группы размещения**: введите имя группы размещения.
|
||||
- выберите правило размещения.
|
||||
- **Зона доступности**: выберите зону доступности, в которой будут создаваться виртуальные серверы по правилу размещения.
|
||||
- добавьте тег группе размещения при необходимости.
|
||||
7. Нажмите **Создать группу**.
|
||||
|
||||
## Редактировать группу размещения
|
||||
|
||||
В группе размещения можно изменить название группы и редактировать теги.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Группы размещения**.
|
||||
3. Переименовать группу размещения:
|
||||
- Нажмите на название группы в списке групп.
|
||||
- Нажмите на … и выберите **Переименовать**.
|
||||
- Введите новое имя группы размещения.
|
||||
- Нажмите ✓.
|
||||
4. Редактировать теги группы размещения:
|
||||
- Нажмите на название группы в списке групп.
|
||||
- Нажмите **Редактировать теги**.
|
||||
- Добавьте или удалите теги.
|
||||
- Нажмите **Сохранить**.
|
||||
|
||||
## Удалить группу размещения
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Группы размещения**.
|
||||
3. Справа от названия группы размещения нажмите кнопку .
|
||||
@@ -0,0 +1,107 @@
|
||||
# Диски
|
||||
|
||||
Хранение данных организовано с использованием сетевых дисков. Диск создается в определенной зоне доступности. Каждый диск автоматически реплицируется внутри своей зоны доступности, что обеспечивает надежное хранение данных.
|
||||
|
||||
Загрузочный диск создается вместе с виртуальным сервером. Конфигурация загрузочного диска задается на этапе [создания виртуального сервера](compute-servers-create.md#создать-сервер). При [удалении виртуального сервера](compute-servers-manage.md#удалить-сервер) загрузочный диск удалится вместе с сервером.
|
||||
|
||||
[Дополнительные диски можно добавить](compute-servers-create.md#добавить-диск) на этапе создания виртуального сервера или [создать диск позже и подключить к нужному виртуальному серверу](#создать-диск). При [удалении виртуального сервера](compute-servers-manage.md#удалить-сервер) дополнительные диски (не загрузочные) останутся в проекте в списке дисков.
|
||||
|
||||
## Посмотреть список дисков
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Диски**.
|
||||
|
||||
## Посмотреть информацию о диске
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Диски**.
|
||||
3. Нажмите на имя диска.
|
||||
4. На странице показана информация о диске:
|
||||
- **Идентификатор**: уникальный идентификатор диска.
|
||||
- **Тип хранения**: тип диска.
|
||||
- **Загрузочный**: является ли диск загрузочным.
|
||||
- **Размер**: размер диска.
|
||||
- **Подключен к**: виртуальный сервер, к которому подключен диск.
|
||||
- **Имя устройства**: имя устройства в файловой системе.
|
||||
|
||||
## Создать диск
|
||||
|
||||
Создать диск дополнительный (не загрузочный):
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Диски**.
|
||||
3. Нажмите **Создать диск**.
|
||||
4. Введите параметры добавляемого диска:
|
||||
- **Имя диска**: введите название диска
|
||||
- **Зона доступности**: выберите из списка зону доступности, в которой будет создан диск.
|
||||
- **Тип**: выберите из списка тип хранения.
|
||||
- **Размер диска**: введите размер добавляемого диска в Гб.
|
||||
5. Нажмите **Создать диск**.
|
||||
|
||||
На странице **Диски** будет добавлен новый диск, но не подключен к какому-либо серверу. Диск можно [подключить к серверу](#подключить-диск-к-виртуальному-серверу).
|
||||
|
||||
## Увеличить размер дискового пространства
|
||||
|
||||
::: warning Важно
|
||||
Изменение размера дискового пространства возможно только в большую сторону.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте диск, размер которого требуется изменить, одним из способов:
|
||||
- На странице **Серверы**:
|
||||
- Откройте **Облачные вычисления → Серверы**.
|
||||
- Нажмите на имя сервера.
|
||||
- Перейдите на вкладку **Диски**.
|
||||
- Нажмите на имя диска и перейдите на шаг 3.
|
||||
- На странице **Диски**:
|
||||
- Откройте **Облачные вычисления → Диски**.
|
||||
- Нажмите на имя диска и перейдите на шаг 3.
|
||||
3. Нажмите **Изменить размер диска**.
|
||||
3. Введите размер добавляемого дискового пространства в Гб.
|
||||
4. Нажмите **Сохранить**.
|
||||
|
||||
Далее требуется увеличить размер диска в ОС.
|
||||
|
||||
## Подключить диск к виртуальному серверу
|
||||
|
||||
Подключить диск к виртуальному серверу можно внутри одной зоны доступности.
|
||||
|
||||
::: warning Важно
|
||||
К виртуальному серверу можно подключить 28 дисков, включая системный.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Нажмите на имя сервера.
|
||||
4. Перейдите на вкладку **Диски**.
|
||||
5. Нажмите **Подключить диск**.
|
||||
6. В строке поиска введите имя диска. Для отображения списка дисков щелкните в строке поиска левой кнопкой мыши.
|
||||
7. Нажмите **Подключить**.
|
||||
|
||||
## Отключить диск от виртуального сервера
|
||||
|
||||
Отключить от виртуального сервера можно только дополнительный диск. Отключение загрузочного диска невозможно.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Нажмите на имя сервера.
|
||||
4. Перейдите на вкладку **Диски**.
|
||||
5. Нажмите на … и выберите **Отключить от сервера**.
|
||||
7. В открывшемся окне подтвердите операцию:
|
||||
- Введите имя виртуального сервера, от которого отключаете диск.
|
||||
- Нажмите **Отключить диск**.
|
||||
|
||||
## Удалить диск
|
||||
|
||||
Перед удалением отключите диск от виртуального сервера.
|
||||
|
||||
::: danger Предупреждение
|
||||
Удаление диска необратимо. Все данные будут удалены без возможности восстановления.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Диски**.
|
||||
3. Нажмите на … и выберите **Удалить**.
|
||||
6. В открывшемся окне подтвердите операцию:
|
||||
- Введите имя удаляемого диска.
|
||||
- Нажмите **Удалить**.
|
||||
@@ -0,0 +1,80 @@
|
||||
# IP-адрес
|
||||
|
||||
IP-адрес — это вычислительный ресурс публичного облака. В облачных сервисах используются внутренние IPv4-адреса.
|
||||
|
||||
Внутренние IP-адреса назначаются автоматически или выбираются из списка зарезервированных адресов при создании виртуального сервера.
|
||||
|
||||
Список зарезервированных адресов формируется из:
|
||||
- IP-адресов, [созданных вручную](#создать-ip-адрес);
|
||||
- IP-адресов, [сохраненных в проекте](#сохранить-ip-адрес-в-проекте).
|
||||
|
||||
::: warning Важно
|
||||
Привязка и отвязка IP-адресов возможна в одной [зоне доступности](../../admin/availability-matrix.md).
|
||||
:::
|
||||
|
||||
## Статусы IP-адресов
|
||||
|
||||
`Используется` — IP-адрес зарезервирован и назначен виртуальному серверу.
|
||||
|
||||
`Зарезервирован` — IP-адрес зарезервирован и не назначен виртуальному серверу, доступен для назначения.
|
||||
|
||||
## Посмотреть список IP-адресов
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → IP-адреса**.
|
||||
3. Отобразится список всех IP-адресов проекта.
|
||||
|
||||
## Создать IP-адрес
|
||||
|
||||
Вы можете зарезервировать IP-адрес из диапазона IP-адресов и назначить этот адрес новому виртуальному серверу.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → IP-адреса**.
|
||||
3. Нажмите **Создать IP-адрес**.
|
||||
4. Введите параметры адреса:
|
||||
- **Имя**: введите имя IP-адреса.
|
||||
- **Зона доступности**: выберите зону доступности, в которой будут доступен адрес.
|
||||
- добавьте тег адресу при необходимости.
|
||||
7. Нажмите **Создать**.
|
||||
|
||||
На странице **Облачные вычисления → IP-адреса** появится новый IP-адрес со статусом `Зарезервирован`.
|
||||
|
||||
## Сохранить IP-адрес в проекте
|
||||
|
||||
При удалении сервера его IP-адрес освобождается и не может быть повторно использован в проекте. Если вам требуется сохранить IP-адрес сервера и привязать этот адрес новому серверу, то отключите автоудаление адреса. IP-адрес сохранится в вашем проекте и может быть назначен новому серверу.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → IP-адреса**.
|
||||
3. Нажмите на имя адреса в списке IP-адресов.
|
||||
4. На странице адреса нажмите кнопку **Изменить**, расположенную ниже флажка **Автоудаление**.
|
||||
5. Снимите флажок **Автоудаление**, если он установлен.
|
||||
6. Нажмите **Сохранить**.
|
||||
|
||||
## Присвоить имя IP-адресу
|
||||
|
||||
Имя IP-адреса отображается только на странице **Облачные вычисления → IP-адреса**. На странице виртуального сервера отображается значение IP-адреса.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → IP-адреса**.
|
||||
3. Нажмите на имя адреса в списке IP-адресов.
|
||||
4. Нажмите на … и выберите **Переименовать**.
|
||||
5. Введите новое имя IP-адреса.
|
||||
6. Нажмите ✓.
|
||||
|
||||
## Удалить IP-адрес
|
||||
|
||||
Если для IP-адреса установлено автоудаление, то адрес удалится во время удаления виртуального сервера.
|
||||
|
||||
Если для IP-адреса не установлено автоудаление, то адрес удаляется вручную:
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → IP-адреса**.
|
||||
3. Удалите адрес одним из способов:
|
||||
- В разделе **IP-адреса**:
|
||||
- В списке адресов найдите IP-адрес, который необходимо удалить.
|
||||
- Cправа от имени IP-адреса нажмите кнопку .
|
||||
- Подтвердите действие.
|
||||
- На странице IP-адреса:
|
||||
- Нажмите на имя адреса в списке IP-адресов., который необходимо удалить.
|
||||
- Нажмите на … в правом углу страницы и выберите **Удалить**.
|
||||
- Подтвердите действие.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Виртуальные серверы
|
||||
|
||||
## Выбор образа операционной системы
|
||||
|
||||
Для проектов уже подготовлены образы на базе операционной системы UNIX.
|
||||
|
||||
| Образ | OC | slug | Версия | Минимальный размер системного диска |
|
||||
|--------------|----------------|-------------------------|----------|-------------------------------------|
|
||||
| Ubuntu | Ubuntu | ubuntu-22-04 | 22.04 | 40 Гб |
|
||||
|
||||
|
||||
|
||||
## Создать сервер
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте раздел **Облачные вычисления**.
|
||||
3. Нажмите **Создать сервер**.
|
||||
4. В блоке **Имя и расположение** укажите имя и расположение виртуального сервера:
|
||||
- **Имя сервера**: введите название виртуального сервера:
|
||||
- допустимая длина 63 символа с учетом зоны DNS проекта;
|
||||
- **Зона доступности**: выберите подходящий [регион ЦОД](../../admin/availability-matrix.md).
|
||||
- Укажите количество создаваемых серверов.
|
||||
5. В блоке **Выбор образа** выберите тип и версию операционной системы.
|
||||
6. В блоке **Конфигурация** выберите количество ОЗУ и ЦПУ. Наборы ресурсов заранее скомпонованы по оптимальным параметрам.
|
||||
::: tip Информация
|
||||
При выборе ресурсов рекомендуется в первую очередь ориентироваться на требуемое количество ОЗУ.
|
||||
:::
|
||||
7. В блоке **Диски** настройте загрузочный диск и, при необходимости, добавьте дополнительные диске:
|
||||
- **Загрузочный диск**: выберите тип хранения и укажите размер загрузочного диска.
|
||||
- Рекомендуется создать дополнительный диск для размещения ваших данных: нажмите **Добавьте диск** и настройте дополнительный диск. [Дополнительный диск можно создать](#добавить-диск) позже.
|
||||
::: warning Важно
|
||||
К виртуальному серверу можно подключить не более 28 дисков, включая системный.
|
||||
:::
|
||||
8. Выберите SSH-ключи, с помощью которых вы можете подключаться к виртуальному серверу по SSH. Если подходящий SSH-ключ отсутствует, то нажмите [**Создать ключ**](../../admin/ssh.md#создать-ssh-ключ).
|
||||
9. В блоке **Настройки сети** выберите:
|
||||
- **IPv4-адрес**: выберите способ получения внутреннего IP-адреса для виртуального сервера:
|
||||
- выберите **Автоматически**, чтобы получить IP-адрес автоматически;
|
||||
- выберите адрес из списка. В списке адресов отображаются [созданные вручную IP-адреса](compute-ip.md#создать-ip-адрес) и [сохраненные IP-адреса](compute-ip.md#сохранить-ip-адрес-в-проекте), которые доступны для переиспользования.
|
||||
10. В блоке **Размещения** выберите:
|
||||
- **Группа размещения**: выберите правило размещения сервера на физическом хосте. [Группу размещения](compute-affinity.md) создайте заранее.
|
||||
11. Установите флажок **Расширенные настройки**, чтобы использовать [cloud-init](https://cloudinit.readthedocs.io/en/latest/) для настройки виртуального сервера.
|
||||
12. Нажмите **Создать сервер**.
|
||||
|
||||
Виртуальный сервер появится на странице **Облачные вычисления → Серверы** в статусе `Cоздается`. Выполняется сборка виртуального сервера, назначается [IP-адрес](compute-ip.md) и полное доменное имя (FQDN). После окончания сборки сервер перейдет в статус `Включен`.
|
||||
|
||||
## Добавить диск
|
||||
|
||||
Добавление дополнительного диска к виртуальному серверу состоит из двух шагов:
|
||||
|
||||
- 1 шаг. Добавить новое устройство.
|
||||
- 2 шаг. Подключить диск внутри операционной системы.
|
||||
|
||||
Новое устройство добавляется в консоли управления:
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
4. Откройте **Облачные вычисления → Серверы**.
|
||||
5. Нажмите на имя сервера.
|
||||
6. Перейдите на вкладку **Диски**.
|
||||
7. Нажмите **Добавить диск**.
|
||||
8. Введите параметры добавляемого диска:
|
||||
- **Имя диска**: введите название диска.
|
||||
- **Тип**: выберите из списка тип хранения.
|
||||
- **Размер диска**: введите размер добавляемого диска в Гб.
|
||||
9. Нажмите **Создать диск**.
|
||||
|
||||
Далее требуется подключить диск в операционной системе.
|
||||
|
||||
## Подключиться к виртуальному серверу
|
||||
|
||||
Подключиться к серверу по протоколу SSH может пользователь с ролью **DevOps-инженер**.
|
||||
|
||||
Для подключения к виртуальному серверу по SSH выполните команду в терминале:
|
||||
|
||||
- по IP-адресу сервера:
|
||||
|
||||
```
|
||||
ssh -l <логин пользователя> -i <путь до приватного ключа> <IP-адрес сервера>
|
||||
```
|
||||
- по полному доменному имени сервера (FQDN):
|
||||
|
||||
```
|
||||
ssh -l <логин пользователя> -i <путь до приватного ключа> <FQDN сервера>
|
||||
```
|
||||
|
||||
Пример команды подключения к серверу по IP-адресу:
|
||||
|
||||
```
|
||||
$ ssh -l ivanov -i /home/user/.ssh/id_rsa 10.0.0.1
|
||||
```
|
||||
@@ -0,0 +1,145 @@
|
||||
# Управление виртуальными серверами
|
||||
|
||||
## Посмотреть список серверов
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
|
||||
## Посмотреть свойства сервера
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Нажмите на имя сервера.
|
||||
4. На вкладке **Общая информация** показана информация о виртуальном сервере:
|
||||
- **Идентификатор**: уникальный идентификатор сервера.
|
||||
- **Внутренний FQDN**: полное доменное имя сервера.
|
||||
- **IPv4 адрес**:внутренний IP-адрес, присвоенный серверу.
|
||||
- **Образ**: операционная система, установленная на сервере.
|
||||
- **Конфигурация**: конфигурация ЦПУ и ОЗУ.
|
||||
- **Группа размещения**: группа размещения сервера.
|
||||
- **Дата создания**: дата и время создания сервера.
|
||||
- **Создатель**: имя пользователя, который создал сервер.
|
||||
- **Теги**: теги, присвоенные серверу.
|
||||
5. На вкладке **Диски** показан загрузочный диск и дополнительные диски, подключенных к серверу.
|
||||
|
||||
## Изменить конфигурацию сервера
|
||||
|
||||
У виртуального сервера можно изменить конфигурацию ЦПУ и ОЗУ: увеличить или уменьшить количество вычислительных ресурсов. Выбор конфигурации предоставляется из линейки доступных тарифов.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. На странице **Обзор** убедитесь в наличии свободных ресурсов.
|
||||
3. Откройте **Облачные вычисления → Серверы**.
|
||||
4. Нажмите на имя сервера.
|
||||
5. На вкладке **Общая информация** нажмите **Изменить конфигурацию**.
|
||||
6. Выберите из списка новый тариф: количество ЦПУ и ОЗУ.
|
||||
7. Нажмите **Сохранить и перезагрузить**.
|
||||
8. Подтвердите действие, нажав **Перезагрузить**.
|
||||
|
||||
Во время выполнения масштабирования сервер находится статусе `Расширение`. После применения изменений сервер будет автоматически перезагружен. Виртуальный сервер перейдет в статус `Включен`.
|
||||
|
||||
## Выключить сервер
|
||||
|
||||
Выключение виртуального сервера не предполагает освобождение вычислительных ресурсов, зарезервированных за этим сервером.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Измените состояние виртуального сервера одним из способов:
|
||||
- На странице **Серверы**:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на … и выберите **Выключить**.
|
||||
- Подтвердите действие, нажав **Выключить**.
|
||||
- На странице виртуального сервера:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на имя сервера.
|
||||
- Нажмите **Выключить**.
|
||||
- Подтвердите действие, нажав **Выключить сервер**.
|
||||
|
||||
Выключение виртуального сервера занимает некоторое время, отключается питание сервера. После выключения сервер переходит в статус `Выключен пользователем`.
|
||||
|
||||
## Включить сервер
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Измените состояние виртуального сервера одним из способов:
|
||||
- На странице **Серверы**:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на … и выберите **Включить**.
|
||||
- На странице виртуального сервера:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на имя сервера.
|
||||
- Нажмите **Включить**.
|
||||
|
||||
Включение виртуального сервера занимает некоторое время. После включения сервер переходит в статус `Включен`.
|
||||
|
||||
## Перезагрузить сервер
|
||||
|
||||
Перезагрузка виртуального сервера предполагает корректное завершение работы операционный системы без отключения питания.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Измените состояние виртуального сервера одним из способов:
|
||||
- На странице **Серверы**:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на … и выберите **Перезагрузить**.
|
||||
- Подтвердите действие, нажав **Перезагрузить**.
|
||||
- На странице виртуального сервера:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на имя сервера.
|
||||
- Нажмите **Перезагрузить**.
|
||||
- Подтвердите действие, нажав **Перезагрузить**.
|
||||
|
||||
Во время выполнения перезагрузки сервер находится в статусе `Перезагружается`. После завершения перезагрузки сервер перейдет в статус `Включен`.
|
||||
|
||||
## Принудительная перезагрузка сервера
|
||||
|
||||
Принудительная перезагрузка виртуального сервера предполагает аппаратное выключение и включение. Несохраненные данные могут быть потеряны.
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Измените состояние виртуального сервера одним из способов:
|
||||
- На странице **Серверы**:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на … и выберите **Принудительная перезагрузка**.
|
||||
- Подтвердите действие, нажав **Перезагрузить**.
|
||||
- На странице виртуального сервера:
|
||||
- В списке виртуальных серверов найдите сервер, состояние которого необходимо изменить.
|
||||
- Нажмите на имя сервера.
|
||||
- Нажмите на … в правом углу страницы и выберите **Принудительная перезагрузка**.
|
||||
- Подтвердите действие, нажав **Перезагрузить**.
|
||||
|
||||
Во время выполнения перезагрузки сервер находится в статусе `Холодная перезагрузка`. После завершения перезагрузки сервер перейдет в статус `Включен`.
|
||||
|
||||
## Назначить виртуальному серверу внутренний IP-адрес
|
||||
|
||||
Если вам необходимо переиспользовать IP-адрес, то перед удалением сервера [сохраните IP-адрес](compute-ip.md#сохранить-ip-адрес-в-проекте). При создании виртуального сервера этот IP-адрес будет доступен для назначения.
|
||||
|
||||
::: warning Важно
|
||||
Переиспользование IP-адресов возможно внутри одной зоны доступности.
|
||||
|
||||
IP-адрес можно назначить новому виртуальному серверу. Назначение IP-адреса существующему виртуальному серверу не предусмотрено.
|
||||
:::
|
||||
|
||||
## Удалить сервер
|
||||
|
||||
После удаления виртуального сервера освобождаются вычислительные ресурсы.
|
||||
|
||||
Системный диск будет удален вместе с сервером. Если к виртуальному серверу подключены дополнительные диски, то при удалении сервера диски будут отключены. В дальнейшем эти диски можно подключить к другому виртуальному серверу.
|
||||
|
||||
IP-адрес будет удален вместе с сервером. Чтобы оставить IP-адрес, перед удалением сервера [сохраните IP-адрес в проекте](../compute-instructions/compute-ip.md#сохранить-ip-адрес-в-проекте). Сохраненный IP-адрес после удаления сервера остается в вашем проекте и будет доступен для назначения новому серверу.
|
||||
|
||||
::: danger Предупреждение
|
||||
Удаление сервера необратимо. Все данные будут удалены без возможности восстановления.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Облачные вычисления → Серверы**.
|
||||
3. Удалите виртуальный сервер одним из способов:
|
||||
- На странице **Серверы**:
|
||||
- В списке виртуальных серверов найдите сервер, которого необходимо удалить.
|
||||
- Нажмите на … и выберите **Удалить**.
|
||||
- Введите имя удаляемого сервера и нажмите **Удалить сервер**.
|
||||
- На странице виртуального сервера:
|
||||
- В списке виртуальных серверов найдите сервер, которого необходимо удалить.
|
||||
- Нажмите на имя сервера.
|
||||
- Нажмите на … в правом углу страницы и выберите **Удалить**.
|
||||
- Введите имя удаляемого сервера и нажмите **Удалить сервер**.
|
||||
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 803 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 996 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 937 B |
@@ -0,0 +1,5 @@
|
||||
# Квоты и лимиты
|
||||
|
||||
Для [проекта действуют квоты](../admin/limits.md), по умолчанию определяемые при инициализации проекта.
|
||||
|
||||
[Изменить квоты](../admin/limits.md#редактирование-квот-проекта) можно на странице **Обзор**.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Уровень обслуживания
|
||||
|
||||
Настоящий документ определяет уровень обслуживания (SLA) сервиса «Облачные вычисления».
|
||||
|
||||
## Описание предоставляемых услуг
|
||||
|
||||
В таблице рассмотрены услуги, предоставляемые в рамках OLA сервиса «Облачные вычисления».
|
||||
|
||||
| Наименование услуги | Краткое описание | Ценность для потребителя | Целевая аудитория | Подразделение-исполнитель|
|
||||
|---|---|---|---|---|
|
||||
| Заказ и управление параметрами виртуальных серверов с ОС | Заказ и управление виртуальной машиной с серверной ОС | Реализация проектной и операционной деятельности продуктовых команд в части ИТ-Инфраструктуры | Руководители проектов<br> DevOps<br> Администраторы| Отдел виртуальных платформ<br> Отдел программно-определяемых сред|
|
||||
|
||||
|
||||
## Доступность услуги
|
||||
|
||||
Заказ и управление параметрами виртуальных серверов с ОС: 99,5 %.
|
||||
|
||||
|
||||
## Характеристики доступности услуги
|
||||
|
||||
Заказ и управление параметрами виртуальных серверов с ОС:
|
||||
| Метрика | Допустимое значение | Измерение |
|
||||
|---|---|---|
|
||||
| HDD IOPS | Эталонные значения:<br>250 IOPS/500GB тип диска «hdd», не более 1000 iops на диск;<br>1000 IOPS/500GB тип диска «ssd/nvme» не более 15000 iops на диск | - Стандартное измерение платформа наблюдаемости «Grafana»<br>- Утилита fio при записи в 32-64 потока c флагом sync в зависимости от размера блока (обычно - 4k/8k)<br>fio -direct=1 -group_reporting -name=test -bs=4k -iodepth=1 -numjobs 1 -rw=randwrite -runtime=60 -filename=/vdb -size=90G |
|
||||
| Количество MIPS на одно vCPU | Не менее 1900 | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| RAM Swaped процент от сконфигурированной памяти VM | До 10% | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| HDD IOPS уменьшение в процентах от оговоренного значения | До 10% | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| Среднее время доступа к диску тип диска «стандартный» на виртуальной машине | До 25 мс на каждый диск (на не нагруженной ВМ) | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| Среднее время доступа к диску тип диска «быстрый» на виртуальной машине | До 8 мс на каждый диск (на не нагруженной ВМ) | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| Среднее время доступа к объекту в «холодном» S3 хранилище read (чтение) | До 30 мс при запросе размером не более 16 Kb | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| Среднее время доступа к объекту в «холодном» S3 хранилище write (запись) | До 110 мс при запросе размером не более 16 Kb | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
|
||||
Предоставление услуги сетевой связности внутри облачных ВМ в пределах ЦОД:
|
||||
| Метрика | Допустимое значение | Измерение |
|
||||
|---|---|---|
|
||||
| Процент потерянных пакетов в пределах ЦОД | Не более 0,2 % | Стандартное измерение платформа наблюдаемости «Grafana» |
|
||||
| Средняя сетевая задержка в пределах ЦОД | Не более 5 мс | Измеряется на эталонной виртуальной машине в проекте команды. Загрузка сетевого интерфейса не более 400 Mb/s |
|
||||
@@ -0,0 +1,82 @@
|
||||
# Обзор сервиса
|
||||
|
||||
Публичное облако предоставляет масштабируемые вычислительные мощности для создания и управления виртуальными серверами.
|
||||
|
||||
К виртуальным серверам можно подключать диски с образами ОС семейства Linux.
|
||||
|
||||
Для сервиса «Облачные вычисления» действует соглашение об [уровне обслуживания](compute-ola.md).
|
||||
|
||||
## Виртуальный сервер
|
||||
|
||||
Конфигурация виртуального сервера задается при его создании:
|
||||
|
||||
- операционная система;
|
||||
- количество ЦПУ;
|
||||
- объем ОЗУ;
|
||||
- тип и размер диска;
|
||||
- регион доступности.
|
||||
|
||||
## Размещение серверов
|
||||
|
||||
Группу серверов внутри кластера можно объединить в соответствии с определенной политикой размещения. Доступны политики:
|
||||
|
||||
- Правило `Affinity` размещает серверы обязательно на одном физическом хосте.
|
||||
|
||||
- Правило `Soft-Affinity` размещает серверы по возможности на одном физическом хосте.
|
||||
|
||||
- Правило `Anti-Affinity` размещает серверы обязательно на разных физических хостах. Такое размещение повышает производительность и предотвращает недоступность сервера при отказе хоста сервера.
|
||||
|
||||
- Правило `Soft-Anti-Affinity` размещает серверы по возможности на разных физических хостах.
|
||||
|
||||
## CPU и RAM
|
||||
|
||||
### Тарифы
|
||||
|
||||
При создании виртуального сервера доступны готовые конфигурации ЦПУ и ОЗУ.
|
||||
|
||||
| Группа | slug | ЦПУ | ОЗУ Гбайт |
|
||||
|:------------|:------------|:----|:----------|
|
||||
| маленькие | cpu2ram2 | 2 | 2 |
|
||||
| маленькие | cpu2ram4 | 2 | 4 |
|
||||
| маленькие | cpu2ram8 | 2 | 8 |
|
||||
| маленькие | cpu2ram16 | 2 | 16 |
|
||||
| маленькие | cpu4ram4 | 4 | 4 |
|
||||
| маленькие | cpu4ram8 | 4 | 8 |
|
||||
| маленькие | cpu4ram16 | 4 | 16 |
|
||||
| маленькие | cpu4ram32 | 4 | 32 |
|
||||
| средние | cpu8ram16 | 8 | 16 |
|
||||
| средние | cpu8ram32 | 8 | 32 |
|
||||
| средние | cpu8ram64 | 8 | 64 |
|
||||
| средние | cpu8ram128 | 8 | 128 |
|
||||
| средние | cpu8ram256 | 8 | 256 |
|
||||
| средние | cpu12ram256 | 12 | 256 |
|
||||
| средние | cpu16ram4 | 16 | 4 |
|
||||
| средние | cpu16ram8 | 16 | 8 |
|
||||
| средние | cpu16ram16 | 16 | 16 |
|
||||
| средние | cpu16ram32 | 16 | 32 |
|
||||
| средние | cpu16ram64 | 16 | 64 |
|
||||
| средние | cpu16ram128 | 16 | 128 |
|
||||
| средние | cpu16ram160 | 16 | 160 |
|
||||
| средние | cpu16ram512 | 16 | 512 |
|
||||
| большие | cpu24ram48 | 24 | 48 |
|
||||
| большие | cpu24ram96 | 24 | 96 |
|
||||
| большие | cpu24ram256 | 24 | 256 |
|
||||
| большие | cpu32ram64 | 32 | 64 |
|
||||
| большие | cpu32ram128 | 32 | 128 |
|
||||
| большие | cpu32ram256 | 32 | 256 |
|
||||
| большие | cpu32ram512 | 32 | 512 |
|
||||
| большие | cpu64ram128 | 64 | 128 |
|
||||
| большие | cpu64ram256 | 64 | 256 |
|
||||
| большие | cpu64ram512 | 64 | 512 |
|
||||
|
||||
## Диски
|
||||
|
||||
Хранение данных организовано с использованием сетевых дисков. Диск создается в определенной зоне доступности. Каждый диск автоматически реплицируется внутри своей зоны доступности, что обеспечивает надежное хранение данных.
|
||||
|
||||
Публичное облако поддерживает типы дисков:
|
||||
- NVME.
|
||||
|
||||
|
||||
## Операционная система
|
||||
|
||||
При создании виртуального сервера можно выбрать операционную систему семейства Unix.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Ресурсные записи
|
||||
|
||||
Ресурсная запись — это запись о соответствии доменного имени и IP-адреса и другой информации в системе доменных имен. Ресурсные записи хранятся на DNS-серверах. DNS-серверы выполняют маршрутизацию запросов, поступающих на определенные доменные имена.
|
||||
|
||||
Сервис DNS поддерживает работу с типами записей:
|
||||
|
||||
`A` — сопоставление доменного имени и IPv4-адреса.
|
||||
|
||||
`CNAME` — синоним FQDN.
|
||||
|
||||
`PTR` — сопоставление IP-адреса и доменного имени.
|
||||
|
||||
У каждой ресурсной записи есть следующие параметры:
|
||||
|
||||
- зона DNS;
|
||||
- тип записи;
|
||||
- доменное имя;
|
||||
- значение записи;
|
||||
- время жизни записи (TTL).
|
||||
|
||||
**TTL (Time to live)** — время жизни записи. TTL определяет, сколько времени запись будет храниться в локальном кэше пользователя. Если вы изменили свойства записи в DNS, то пользователи не увидят этих изменений до тех пор, пока не истечет значение TTL.
|
||||
|
||||
## Создать запись
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **DNS**.
|
||||
3. Нажмите **Добавить**.
|
||||
4. Выберите тип записи: `A`, `CNAME`, `PTR`.
|
||||
5. Заполните параметры записи:
|
||||
|
||||
::: details A
|
||||
- **Зона**: выберите из списка *<slug-проекта>.cloud.dfcloud.ru*.
|
||||
- **Параметр**: введите доменное имя записи. Максимальная длина имени — 63 символа и полного доменного имени (FQDN) — 255 символов.
|
||||
- **Значение**: введите IP-адрес виртуального сервера. IP-адрес можно посмотреть на странице **Облачные вычисления → Серверы**.
|
||||
- **TTL**: введите время жизни записи в секундах.
|
||||
:::
|
||||
|
||||
::: details CNAME
|
||||
- **Зона**: выберите из списка *<slug-проекта>.cloud.dfcloud.ru*.
|
||||
- **Параметр**: введите доменное имя записи.
|
||||
- **Значение**: введите FQDN сервера, точка не ставится в конце значения.
|
||||
- **TTL**: введите время жизни записи в секундах.
|
||||
:::
|
||||
|
||||
::: details PTR
|
||||
|
||||
Запись в обратной зоне добавляется автоматически при создании виртуального сервера. Для одного IP-адреса предназначена одна PTR-запись. Если для нужного IP-адреса не создана PTR-запись, то создайте ее вручную.
|
||||
|
||||
- **Зона**: выберите из списка *<slug-проекта>.cloud.dfcloud.ru*.
|
||||
- **Параметр**: введите IP-адрес виртуального сервера. IP-адрес можно посмотреть на странице **Облачные вычисления → Серверы**.
|
||||
- **Значение**: введите FQDN сервера.
|
||||
- **TTL**: введите время жизни записи в секундах.
|
||||
:::
|
||||
|
||||
2. Нажмите **Добавить**.
|
||||
|
||||
## Редактировать запись
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **DNS**.
|
||||
3. Откройте записи прямой или обратной зоны.
|
||||
4. В таблице записей напротив нужной записи нажмите **Редактировать**.
|
||||
5. Измените `Параметр`, `Значение` или `TTL` записи.
|
||||
6. Нажмите **Сохранить**.
|
||||
|
||||
## Удалить запись
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **DNS**.
|
||||
3. Откройте записи прямой или обратной зоны.
|
||||
4. В таблице записей напротив нужной записи нажмите **Удалить**.
|
||||
5. В открывшемся окне подтвердите операцию:
|
||||
- Скопируйте предложенный текст.
|
||||
- Нажмите **Удалить**.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Квоты и лимиты
|
||||
|
||||
Ограничений на количество создаваемых записей в зоне нет.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Обзор сервиса
|
||||
|
||||
Сервис DNS предназначен для управления доменными именами ресурсов в проекте.
|
||||
|
||||
Сервис DNS предоставляет одну зону в `cloud.dfcloud.ru`.
|
||||
|
||||
Для каждого проекта автоматически создается зона: `<slug-проекта>.cloud.dfcloud.ru`. В этой зоне можно [управлять записями](../dns/dns-instructions/dns-create.md): создавать, редактировать, удалять.
|
||||
|
||||
При создании виртуального сервера автоматически добавляются:
|
||||
- FQDN сервера в прямой зоне;
|
||||
- PTR-запись в обратной зоне.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Обзор облачной платформы
|
||||
|
||||
**Публичное облако** — это облачная платформа, предоставляющая инфраструктурные сервисы IaaS, PaaS на базе платформ виртуализации OpenStack, VMware, Hyper-V в режиме самообслуживания или посредством программного интерфейса API.
|
||||
|
||||
**Infrastructure-as-a-Service (IaaS)** или инфраструктура как услуга — это модель предоставления облачных вычислений, при которой потребители получают доступ к управлению полным жизненным циклом, а также необходимый доступ к фундаментальным ИТ-ресурсам: серверы с заданной вычислительной мощностью и емкостью хранения, типовой преднастроенной операционной системой, подключенные к сети.
|
||||
|
||||
**Platform-as-a-Service (PaaS)** или платформа как услуга — это модель предоставления облачных вычислений, при которой потребители получают доступ к использованию ИТ-платформой: операционные системы, системы управления базами данных, сервисные шины, вызов удаленных процедур, брокеры сообщений и т.п. При этом управление низлежащим слоем потребителю недоступно.
|
||||
|
||||
**Infrastructure-as-Code (IaC)** или инфраструктура как код — это подход для управления и описания инфраструктуры посредством конфигурационных файлов без непосредственного взаимодействия с интерфейсами управления.
|
||||
@@ -0,0 +1,86 @@
|
||||
# S3cmd
|
||||
|
||||
S3cmd — это консольный клиент (Linux, Mac) для работы с Amazon S3-совместимыми хранилищами.
|
||||
|
||||
Подробности о работе c s3cmd см. [в документации s3cmd](https://s3tools.org/s3cmd-howto).
|
||||
|
||||
## Подготовительные шаги
|
||||
|
||||
- [Создайте аккаунт S3 и ключ доступа](../storage-s3.md#создать-аккаунт). Сохраните ключ **secret**.
|
||||
- Установите [S3cmd](https://s3tools.org/download).
|
||||
|
||||
## Настроить подключение
|
||||
|
||||
Перед настройкой подключения [создайте аккаунт и ключ доступа](../storage-s3.md#создать-аккаунт).
|
||||
|
||||
Настроить подключение к хранилищу можно одним из способов:
|
||||
- **1 способ**: установить значения переменных и подключиться к хранилищу;
|
||||
- **2 способ**: настроить подключение с помощью стандартной команды s3cmd `s3cmd --configure`.
|
||||
|
||||
::: tabs
|
||||
|
||||
== 1 способ
|
||||
1. Установите значения переменных:
|
||||
|
||||
```sh
|
||||
S3_ACCESS_KEY=...
|
||||
S3_SECRET_KEY=...
|
||||
S3_URL=...
|
||||
```
|
||||
где:
|
||||
- `S3_ACCESS_KEY`: введите *Access key*, полученный при [создании хранилища в консоли управления](../storage-s3.md#создать-аккаунт).
|
||||
- `S3_SECRET_KEY`: введите ключ *secret*, полученный при [создании хранилища в консоли управления](../storage-s3.md#создать-аккаунт).
|
||||
- `S3_URL`: имя хоста для подключения соответствует [региону](../../storage-overview.md#регионы-и-зоны-доступности).
|
||||
|
||||
2. Подключите хранилище:
|
||||
|
||||
```sh
|
||||
s3cmd --ssl --access_key="${S3_ACCESS_KEY}" --secret_key="${S3_SECRET_KEY}" --host="${S3_URL}" --region=US --no-progress --host-bucket="%(bucket)" --dump-config > ~/.s3cfg
|
||||
```
|
||||
|
||||
== 2 способ
|
||||
1. Выполните команду в терминале:
|
||||
|
||||
```sh
|
||||
s3cmd --configure
|
||||
```
|
||||
2. Укажите значения для следующих параметров:
|
||||
- `Access Key`: введите *Access key*, полученный при [создании хранилища в консоли управления](../storage-s3.md#создать-аккаунт).
|
||||
- `Secret Key`: введите ключ *secret*, полученный при [создании хранилища в консоли управления](../storage-s3.md#создать-аккаунт)
|
||||
- .
|
||||
- `S3 Endpoint`: имя хоста для подключения соответствует [региону](../../storage-overview.md#регионы-и-зоны-доступности).
|
||||
- `DNS-style bucket+hostname:port template for accessing a bucket`: введите `%(bucket)`.
|
||||
- `Use HTTPS protocol`: введите `Y`.
|
||||
3. Остальные настройки оставьте без изменений.
|
||||
4. Устанавливается соединение. В случае успеха, отобразится `Success. Your access key and secret key worked fine :-)`.
|
||||
5. Сохраните конфигурацию: `Save settings? [y/N]`: введите `Y`
|
||||
|
||||
:::
|
||||
|
||||
S3cmd сохранит настройки в файле `~/.s3cfg`, его можно изменить вручную.
|
||||
|
||||
::: warning Важно
|
||||
Если при установке соединения с хранилищем возникла ошибка валидности SSL-сертификатов, то выполните команду в терминале для добавления корневого сертификата:
|
||||
|
||||
```sh
|
||||
echo 'alias s3cmd="s3cmd --ca-certs /etc/ssl/certs/ca-certificates.crt"' >> ~/.profile
|
||||
```
|
||||
Выполните настройку подключения к хранилищу снова.
|
||||
:::
|
||||
|
||||
## Проверить подключение к хранилищу
|
||||
|
||||
Выполните команду в терминале:
|
||||
|
||||
```sh
|
||||
s3cmd ls
|
||||
```
|
||||
В выводе должен отобразиться список доступных бакетов. Список может быть пустым, если в хранилище не создано ни одного бакета.
|
||||
|
||||
## Примеры команд
|
||||
|
||||
- `s3cmd --help` - справка.
|
||||
- `s3cmd mb s3://<имя_бакета>` - создание бакета.
|
||||
- `s3cmd ls s3://<имя_бакета>` - получение списка бакетов.
|
||||
|
||||
Подробности о работе c s3cmd см. [в документации s3cmd](https://s3tools.org/usage).
|
||||
@@ -0,0 +1,44 @@
|
||||
# WinSCP
|
||||
|
||||
WinSCP — это графический клиент, поддерживающий работу с различными типами хранилищ, в том числе с [Amazon S3](https://aws.amazon.com/ru/s3/) совместимыми хранилищами. Доступен для Windows.
|
||||
|
||||
## Настроить подключение
|
||||
|
||||
1. [Создайте аккаунт и ключ доступа](../storage-s3.md#создать-аккаунт).
|
||||
2. Скачайте WinSCP с [официального сайта](https://winscp.net/eng/download.php).
|
||||
3. Откройте WinSCP.
|
||||
4. Cоздайте новое подключение:
|
||||
- **Протокол передачи**: выберите из списка *Amazon S3*.
|
||||
- **Имя хоста**: соответствует [региону](../../storage-overview.md#регионы-и-зоны-доступности).
|
||||
- **Порт**: введите *443*.
|
||||
- **Идентификатор ключа доступа**: введите *Access key*, полученный при создании аккаунта в консоли управления.
|
||||
- **Секретный ключ доступа**: введите ключ *Secret*, полученный при создании аккаунта в консоли управления.
|
||||
5. Нажмите **Еще**.
|
||||
6. В окне **Расширенные настройки соединения** выберите **Среда → S3 → Стиль по умолчанию URL** значение *Путь*.
|
||||
7. Нажмите **OK**.
|
||||
8. Нажмите **Войти**, потребуется еще раз ввести секретный ключ доступа.
|
||||
|
||||
В результате успешного подключения на вкладке справа отобразятся бакеты. Список может быть пустым, если в хранилище не создано ни одного объекта.
|
||||
|
||||
Подробности о работе WinSCP с S3-совместимыми хранилищами см. [в документации WinSCP](https://winscp.net/eng/docs/guide_amazon_s3#buckets).
|
||||
|
||||
## Проверить доступность хранилища
|
||||
|
||||
Для проверки доступности хранилища создайте файл в хранилище и попробуйте его скачать.
|
||||
|
||||
1. Войдите в S3 хранилище.
|
||||
2. В меню **Файлы** выберите **Новый → Каталог**;
|
||||
- введите название каталога по [правилам](../../storage-overview.md#правила-именования);
|
||||
3. Создайте файл в каталоге из п. 2:
|
||||
- в меню **Файлы** выберите **Новый → Файл**;
|
||||
- введите название файла по [правилам](../../storage-overview.md#правила-именования);
|
||||
- сохраните файл.
|
||||
4. Выдайте права на действия с файлом:
|
||||
- правой кнопкой мыши нажмите на имя файла и выберите **Свойства**;
|
||||
- в ACL установите права **R** для всех;
|
||||
- нажмите **OK**.
|
||||
5. Проверьте сетевую доступность файла, созданного на шаге 3:
|
||||
- правой кнопкой мыши нажмите на имя файла и выберите **Файловые пользовательские команды → Сгенерировать URL для протокола HTTP**;
|
||||
- в открывшемся окне нажмите **Копировать**;
|
||||
- вставьте скопированную ссылку в браузер, заменив `http://` на `https://`;
|
||||
- начнется скачивание файла.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Управление хранилищем
|
||||
|
||||
## Создать аккаунт
|
||||
|
||||
При добавлении хранилища выполняется создание аккаунта S3 и ключей доступа.
|
||||
|
||||
::: tip Информация
|
||||
В момент создания нового хранилища сохраните ключи и секреты (токены) доступа. Данная информация не хранится на серверах Vega. В случае утери ключей доступа все данные хранилища будут недоступны.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Объектное хранилище**.
|
||||
3. Нажмите **Добавить хранилище**.
|
||||
4. Заполните параметры хранилища:
|
||||
- **Регион**: выбрать из списка.
|
||||
- **Название аккаунта**: введите название аккаунта. <!--Есть правила нейминга?-->
|
||||
- **Размер хранилища**: введите значение в Гбайтах, размер хранилища ограничен квотой.
|
||||
5. Нажмите **Создать**.
|
||||
6. В открывшемся окне скопируйте ключи и сохраните их в надежном месте.
|
||||
7. Нажмите **Закрыть**.
|
||||
|
||||
Аккаунт S3 будет добавлен в разделе **Объектное хранилище**. Далее необходимо [подключиться к хранилищу c помощью утилиты S3cmd](../storage-instructions/s3-connect/s3cmd.md) или [WinSCP](../storage-instructions/s3-connect/winscp.md).
|
||||
|
||||
## Удалить аккаунт
|
||||
|
||||
::: warning Важно
|
||||
Перед удалением аккаунта удалите бакеты вручную.
|
||||
:::
|
||||
|
||||
1. Перейдите в консоль управления.
|
||||
2. Откройте **Объектное хранилище**.
|
||||
3. Найдите нужное хранилище.
|
||||
5. Нажмите ⠇ и выберите **Удалить**.
|
||||
6. В открывшемся окне подтвердите операцию:
|
||||
- Скопируйте предложенный текст.
|
||||
- Нажмите **Удалить**.
|
||||
|
||||
## Управление жизненным циклом объектов в бакете
|
||||
|
||||
Управление жизненным циклом объектов позволяет настроить автоматическое удаление отдельных объектов или групп объектов по заданным условиям и расписанию.
|
||||
|
||||
Для управления жизненными циклами с помощью S3cmd используется [конфигурация в формате XML](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-configuration-examples.html).
|
||||
|
||||
::: tabs
|
||||
|
||||
== Загрузить политику
|
||||
|
||||
```sh
|
||||
s3cmd setlifecycle bucket-lifecycle.xml s3://bucket
|
||||
```
|
||||
|
||||
== Получить политику
|
||||
|
||||
```sh
|
||||
s3cmd getlifecycle s3://bucket
|
||||
```
|
||||
|
||||
== Удалить политику
|
||||
|
||||
```sh
|
||||
s3cmd dellifecycle s3://bucket
|
||||
```
|
||||
:::
|
||||
|
||||
## Управление доступом (ACL)
|
||||
|
||||
Access Control List (ACL) — список управления доступом, который определяет, список пользователей для доступа к файлам или бакету и список допустимых или запрещенных операции.
|
||||
|
||||
Для управления доступом в S3 хранилище можно использовать команды из [документации S3cmd](https://s3tools.org/s3cmd).
|
||||
|
||||
## Монтирование бакета
|
||||
|
||||
Монтирование бакета позволит управлять содержимым S3-хранилища через интерфейс файловой системы. Бакет объектного хранилища монтируется через [FUSE](https://ru.wikipedia.org/wiki/FUSE_(%D0%BC%D0%BE%D0%B4%D1%83%D0%BB%D1%8C_%D1%8F%D0%B4%D1%80%D0%B0)).
|
||||
|
||||
В инструкции рассмотрено разовое монтирование контейнера. В этом случае при каждой перезагрузке системы контейнер будет размонтирован. Потребуется выполнить монтирование контейнера вручную после перезагрузки системы.
|
||||
|
||||
::: tabs
|
||||
== GeeseFS
|
||||
1. Установите клиент GeeseFS.
|
||||
2. Сделайте файл geesefs исполняемым и поместите его в директорию `/bin`:
|
||||
|
||||
```sh
|
||||
chmod 700 geesefs-linux-amd64
|
||||
mv geesefs-linux-amd64 /bin/geesefs
|
||||
```
|
||||
|
||||
3. [Используйте ключ доступа, полученный при создании аккаунта](#создать-аккаунт), и поместите его в директорию `~/.aws/credentials`:
|
||||
|
||||
```
|
||||
[default]
|
||||
aws_access_key_id = <Access key>
|
||||
aws_secret_access_key = <secret>
|
||||
```
|
||||
4. Создайте директорию, в которой будет отображаться содержимое бакета. Например, `/mnt/s3`.
|
||||
5. Для монтирования бакета выполните команду:
|
||||
|
||||
```sh
|
||||
/bin/geesefs --endpoint <endpoint s3> <имя бакета> /mnt/s3/
|
||||
```
|
||||
== S3fs
|
||||
1. Установите клиент S3fs:
|
||||
|
||||
```sh
|
||||
apt install s3fs
|
||||
```
|
||||
2. Cохраните [ключ доступа и секретный ключ, полученные при создании аккаунта](#создать-аккаунт) в файле `~/.passwd-s3fs`:
|
||||
|
||||
```sh
|
||||
echo <идентификатор_ключа>:<секретный_ключ> > ~/.passwd-s3fs
|
||||
```
|
||||
3. Ограничьте доступ к файлу `~/.passwd-s3fs`:
|
||||
|
||||
```sh
|
||||
chmod 600 ~/.passwd-s3fs
|
||||
```
|
||||
4. Для монтирования бакета выполните команду:
|
||||
|
||||
```sh
|
||||
s3fs <имя_бакета> <путь_к_директории> -o passwd_file=~/.passwd-s3fs -o url=<адрес_endpoint> -o use_path_request_style
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Для проверки монтирования бакета выполните команду:
|
||||
|
||||
```sh
|
||||
df-h
|
||||
```
|
||||
|
||||
### Размонтировать бакет
|
||||
|
||||
Для размонтирования бакета выполните команду:
|
||||
|
||||
```sh
|
||||
umount <путь_к_директории>
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Квоты и лимиты
|
||||
|
||||
Для сервиса действует квота по умолчанию, определяемая при инициализация проекта, в размере 100 Гбайт.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Уровень обслуживания
|
||||
|
||||
Настоящий документ определяет уровень обслуживания (SLA) сервиса «Объектное хранилище».
|
||||
|
||||
|
||||
## Описание предоставляемых услуг
|
||||
|
||||
В таблице рассмотрены услуги, предоставляемые в рамках SLA сервиса «Объектное хранилище».
|
||||
|
||||
| Наименование услуги | Краткое описание | Ценность для потребителя | Целевая аудитория |
|
||||
|---|---|---|---|
|
||||
| Доступ к S3 хранилищу | Web-доступ к эластичному хранилищу данных | Возможность очень быстрого подключения дискового пространства в приложение, сервер, ВРС и т. п. | DevOps<br> Администраторы |
|
||||
|
||||
|
||||
## Доступность услуги
|
||||
|
||||
Доступ к S3 хранилищу 99,5 %.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Обзор сервиса
|
||||
|
||||
Сервис «Объектное хранилище» предназначен для хранения и извлечения данных. Доступ к объектам осуществляется из любых периметров по протоколу https.
|
||||
|
||||
## Регионы и зоны доступности
|
||||
|
||||
| Регион | Хранилище | Адрес |
|
||||
| -------------------------------- | --------- | ---------------------------------------- |
|
||||
| s3-cloud-dfcloud-ru | Ceph | https://s3.cloud.dfcloud.ru |
|
||||
|
||||
|
||||
## Модели адресации в S3
|
||||
|
||||
Сервис поддерживает модель адресации для доступа к объектному хранилищу S3:
|
||||
|
||||
- `Path-style` — модель, при которой название бакета указывается в части пути до объекта, например: https://s3.cloud.dfcloud.ru/bucket/file.txt
|
||||
|
||||
## Правила именования
|
||||
|
||||
Правила именования бакетов:
|
||||
- имя бакета в корневом каталоге должно быть уникальным для всего хранилища S3, на каталоги 2-го и последующих уровней это правило не распространяется;
|
||||
- может содержать строчные буквы латинского алфавита, цифры, дефис и подчеркивания;
|
||||
- подчеркивание недопустимо для имени бакета в корневом каталоге.
|
||||
|
||||
Правила именования файлов в бакете:
|
||||
- может содержать строчные буквы латинского алфавита, цифры, дефис и подчеркивания.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Using Vue in Markdown
|
||||
|
||||
## Browser API Access Restrictions
|
||||
|
||||
Because VuePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the [universal code requirements](https://ssr.vuejs.org/en/universal.html). In short, make sure to only access Browser / DOM APIs in `beforeMount` or `mounted` hooks.
|
||||
|
||||
If you are using or demoing components that are not SSR friendly (for example containing custom directives), you can wrap them inside the built-in `<ClientOnly>` component:
|
||||
|
||||
##
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "BeeCloud Docs"
|
||||
text: "Документация публичного облака"
|
||||
# tagline: My great project tagline
|
||||
|
||||
features:
|
||||
- title: Облачные вычисления
|
||||
details: Масштабируемые вычислительные мощности для создания и управления виртуальными серверами.
|
||||
link: /guide/compute/compute-overview
|
||||
- title: Объектное хранилище
|
||||
details: Amazon совместимое S3 хранилище.
|
||||
link: /guide/storage/storage-overview
|
||||
- title: DNS
|
||||
details: Управление ресурсными записями DNS.
|
||||
link: /guide/dns/dns-overview
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44.9926 89.9896C69.8379 89.9896 89.979 69.8448 89.979 44.9948C89.979 20.1449 69.8379 0 44.9926 0C20.1472 0 0.00610352 20.1449 0.00610352 44.9948C0.00610352 69.8448 20.1472 89.9896 44.9926 89.9896Z" fill="#FFC800"/>
|
||||
<path d="M64.3526 78.5183C49.075 87.3357 32.5061 88.9159 21.9226 83.6342C35.4726 91.7095 52.8704 92.4079 67.4937 83.9659C82.1169 75.5152 90.2137 60.0979 89.9956 44.3226C89.2802 56.1344 79.6302 69.701 64.3526 78.5183Z" fill="black"/>
|
||||
<path d="M50.6979 3.29996C53.7953 8.66023 46.3092 18.7871 33.972 25.9021C21.6347 33.0171 9.13171 34.4489 6.03431 29.0799C5.90343 28.8267 5.78128 28.5473 5.69403 28.2593C4.89133 25.7624 5.85108 22.5323 8.16323 19.1276C8.5384 18.5951 8.92231 18.0713 9.31493 17.5562C12.805 13.0078 17.2286 9.06181 22.4985 6.02375C27.7685 2.97695 33.3961 1.12617 39.0762 0.375387C39.7393 0.279356 40.3849 0.209517 41.0131 0.165867C45.1139 -0.139686 48.3858 0.646022 50.1483 2.5841C50.3577 2.81981 50.5409 3.05552 50.6979 3.29996Z" fill="black"/>
|
||||
<path d="M69.8233 9.10545C75.2329 18.4728 64.3178 34.8941 45.4542 45.7892C26.5906 56.6844 6.90686 57.924 1.50604 48.5567C0.607359 46.994 0.127481 45.2567 0.0315055 43.3885C-0.204071 49.6828 0.877838 56.082 3.39938 62.1581C3.77456 63.0661 4.18464 63.9653 4.62089 64.8557C4.63834 64.8906 4.66452 64.9343 4.68197 64.9692C6.31356 67.789 8.72167 70.0065 11.7231 71.6128C22.3066 77.2786 40.2017 75.3318 56.9364 65.6676C73.6624 56.0034 84.2982 41.4853 84.6909 29.4902C84.8043 26.1029 84.0976 22.9164 82.4834 20.0966C81.8727 19.18 81.2357 18.2808 80.5639 17.4252C76.594 12.2919 71.6556 8.20625 66.1675 5.29041C67.6944 6.29436 68.9334 7.56022 69.8233 9.10545C69.8233 9.10545 69.8321 9.11418 69.8321 9.12291L69.8233 9.10545Z" fill="black"/>
|
||||
<path d="M45.0016 89.9896C69.8469 89.9896 89.9881 69.8448 89.9881 44.9948C89.9881 20.1449 69.8469 0 45.0016 0C20.1563 0 0.0151367 20.1449 0.0151367 44.9948C0.0151367 69.8448 20.1563 89.9896 45.0016 89.9896Z" fill="url(#paint0_radial_994:255)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_994:255" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(44.9974 44.9986) scale(44.9854 45.0112)">
|
||||
<stop offset="0.5" stop-color="white" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.35"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 380 380"
|
||||
version="1.1"
|
||||
id="svg578"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs567">
|
||||
<style
|
||||
id="style565">.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style>
|
||||
</defs>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="m 358.48029,150.34002 -0.48818,-1.24758 -47.26334,-123.347555 a 12.313059,12.313059 0 0 0 -4.86375,-5.858196 12.656595,12.656595 0 0 0 -14.46468,0.777477 12.656595,12.656595 0 0 0 -4.19476,6.364459 l -31.9127,97.636595 H 126.06904 L 94.156344,27.028625 a 12.403463,12.403463 0 0 0 -4.194757,-6.38254 12.656595,12.656595 0 0 0 -14.46468,-0.777477 12.421544,12.421544 0 0 0 -4.863748,5.858196 L 23.279412,149.02012 22.80931,150.2677 a 87.764446,87.764446 0 0 0 29.110169,101.43357 l 0.162727,0.12656 0.433941,0.30738 71.997943,53.91709 35.61928,26.95855 21.69702,16.38125 a 14.591246,14.591246 0 0 0 17.64691,0 l 21.69702,-16.38125 35.61927,-26.95855 72.43189,-54.24255 0.1808,-0.14464 a 87.800608,87.800608 0 0 0 29.07401,-101.32509 z"
|
||||
id="path569"
|
||||
style="stroke-width:1.80808" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="m 358.48029,150.34002 -0.48818,-1.24758 a 159.65391,159.65391 0 0 0 -63.55419,28.56775 l -103.80216,78.48897 c 35.34806,26.74157 66.12167,49.97547 66.12167,49.97547 l 72.43188,-54.24255 0.18081,-0.14465 a 87.800608,87.800608 0 0 0 29.11017,-101.39741 z"
|
||||
id="path571"
|
||||
style="stroke-width:1.80808" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="m 124.51409,306.12463 35.61928,26.95854 21.69702,16.38125 a 14.591246,14.591246 0 0 0 17.64691,0 l 21.69702,-16.38125 35.61927,-26.95854 c 0,0 -30.80977,-23.30622 -66.15783,-49.97547 -35.34806,26.66925 -66.12167,49.97547 -66.12167,49.97547 z"
|
||||
id="path573"
|
||||
style="stroke-width:1.80808" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M 86.815519,177.66019 A 159.45502,159.45502 0 0 0 23.279412,149.02012 l -0.470102,1.24758 a 87.764446,87.764446 0 0 0 29.110169,101.43357 l 0.162727,0.12656 0.433941,0.30738 71.997943,53.91709 c 0,0 30.73745,-23.23389 66.12167,-49.97547 z"
|
||||
id="path575"
|
||||
style="stroke-width:1.80808" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1 @@
|
||||
<g></g>
|
||||
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="1024.000000" height="1024.000000" preserveAspectRatio="xMidYMid meet"><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/>
|
||||
|
||||
|
||||
<g class="currentLayer" style=""><title>Layer 1</title><g transform="translate(0,1024) scale(0.10000000149011612,-0.10000000149011612) " fill="#7e00ed" stroke="none" id="svg_1" class="selected" fill-opacity="1">
|
||||
<path d="M4850 7729 c-519 -46 -1038 -265 -1445 -609 -103 -87 -273 -263 -355 -365 -225 -284 -406 -647 -490 -986 -17 -68 -34 -135 -38 -147 -6 -20 -39 -37 -187 -96 -749 -303 -1248 -606 -1664 -1011 -290 -282 -474 -560 -548 -830 -24 -87 -27 -118 -28 -245 0 -176 23 -275 88 -372 146 -219 531 -340 1132 -355 665 -16 1453 126 1589 287 30 36 34 80 8 103 -15 14 -28 14 -122 2 -305 -40 -464 -49 -860 -50 -428 0 -523 8 -727 61 -260 67 -390 188 -369 342 39 292 588 705 1441 1082 326 144 928 392 1209 497 956 358 1604 515 2131 517 161 1 227 -11 245 -44 13 -24 25 -17 -270 -167 -681 -347 -1305 -736 -1665 -1038 -124 -104 -292 -272 -358 -357 -106 -137 -167 -303 -167 -457 0 -111 18 -183 77 -301 40 -80 62 -110 143 -191 52 -53 129 -119 170 -146 360 -241 953 -381 1459 -345 524 37 991 210 1406 521 127 96 343 306 448 436 252 313 445 711 531 1094 14 63 29 117 33 121 4 4 64 26 133 50 379 130 907 395 1234 618 747 509 1142 1061 1113 1551 -15 251 -105 396 -313 503 -148 76 -304 119 -564 155 -158 22 -707 25 -905 5 -181 -18 -461 -59 -575 -83 -334 -73 -533 -193 -467 -282 11 -16 20 -16 122 -3 309 41 467 50 865 51 427 0 519 -8 726 -60 308 -78 433 -242 344 -451 -51 -118 -210 -287 -404 -428 -237 -173 -640 -392 -1016 -551 -102 -43 -211 -91 -242 -106 -51 -26 -57 -27 -62 -11 -2 9 -14 57 -27 107 -77 316 -246 670 -450 941 -546 727 -1429 1122 -2329 1043z" id="svg_2" fill="#7e00ed" fill-opacity="1"/>
|
||||
</g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,84 @@
|
||||
# Вопросы и ответы
|
||||
|
||||
## Не могу найти ресурс, созданный через Terraform?
|
||||
|
||||
Во время выполнения команды `terraform [apply | plan]` получаем ошибку `404` __Сервер по вашему запросу не найден__:
|
||||
|
||||
```
|
||||
lanning failed. Terraform encountered an error while generating this plan.
|
||||
╷
|
||||
│ Error: url=[https://console.cloud.dfcloud.ru/api/v1/projects/vega-dev-demo/vps/servers/cac9da7e-d3b1-488c-9bb0-277fd20140d1,body=]
|
||||
{"code":404,"error":"server-not-found","description":"Сервер по вашему запросу не найден"} // [!code focus]
|
||||
│
|
||||
│ with beecloud_server.router[1], // [!code focus]
|
||||
│ on main.tf line 23, in resource "beecloud_server" "router":
|
||||
│ 23: resource "beecloud_server" "router" {
|
||||
│
|
||||
```
|
||||
Решение:
|
||||
|
||||
1. Удалите поломанный ресурс из файла состояния:
|
||||
```sh
|
||||
terraform state rm "beecloud_server.router[1]"
|
||||
```
|
||||
Вывод:
|
||||
|
||||
```
|
||||
Removed beecloud_server.router[1]
|
||||
Successfully removed 1 resource instance(s).
|
||||
```
|
||||
2. Повторно примените конфигурацию Terraform:
|
||||
|
||||
```sh
|
||||
terraform apply
|
||||
```
|
||||
|
||||
Terraform создаст недостающий ресурс.
|
||||
|
||||
## Что делать, если при создании ресурса Terraform выдает ошибку: "context deadline exceeded"?
|
||||
|
||||
Во время выполнения команды `terraform apply` получаем ошибку: __context deadline exceeded__
|
||||
|
||||
```
|
||||
beecloud_server.my-server: Creating...
|
||||
...
|
||||
beecloud_server.my-server: Still creating... [10m elapsed]
|
||||
╷
|
||||
│ Error: Error waiting for server ae8c5364-16e4-4b9b-84e1-092a04647838 to come ready, error: context deadline exceeded // [!code focus]
|
||||
│
|
||||
│ with beecloud_server.my-server, // [!code focus]
|
||||
│ on main.tf line 31, in resource "beecloud_server" "my-server":
|
||||
│ 31: resource "beecloud_server" "my-server" {
|
||||
╵
|
||||
```
|
||||
|
||||
Решение:
|
||||
|
||||
Увеличьте тайм-аут ожидания ресурса при создании:
|
||||
|
||||
```hcl
|
||||
resource "beecloud_server" "my-server" {
|
||||
...
|
||||
|
||||
timeouts {
|
||||
create = "30m"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Тайм-ауты по умолчанию для операций:
|
||||
|
||||
| CRUD Function | Default Timeout |
|
||||
|----------------------|-----------------|
|
||||
| Create | 20 minutes |
|
||||
| CreateContext | 20 minutes |
|
||||
| CreateWithoutTimeout | N/A |
|
||||
| Delete | 20 minutes |
|
||||
| DeleteContext | 20 minutes |
|
||||
| DeleteWithoutTimeout | N/A |
|
||||
| Read | 20 minutes |
|
||||
| ReadContext | 20 minutes |
|
||||
| ReadWithoutTimeout | N/A |
|
||||
| Update | 20 minutes |
|
||||
| UpdateContext | 20 minutes |
|
||||
| UpdateWithoutTimeout | N/A |
|
||||
@@ -0,0 +1,31 @@
|
||||
# Terraform
|
||||
|
||||
[Terraform](https://www.terraform.io/) позволяет быстро создать облачную инфраструктуру в Облаке Vega и управлять ей с помощью файлов конфигураций. В файлах конфигураций хранится описание инфраструктуры на языке HCL (HashiCorp Configuration Language). Terraform и его провайдеры распространяются под лицензией [Business Source License](https://github.com/hashicorp/terraform/blob/main/LICENSE).
|
||||
|
||||
При изменении файлов конфигураций Terraform автоматически определяет, какая часть вашей конфигурации уже развернута, что следует добавить или удалить.
|
||||
|
||||
## Установите Terraform
|
||||
|
||||
::: tabs
|
||||
== UNIX
|
||||
Выполните команду в терминале:
|
||||
|
||||
Ubuntu/Debian
|
||||
```sh
|
||||
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||
sudo apt update && sudo apt install terraform
|
||||
```
|
||||
|
||||
== MacOS
|
||||
Выполните команду в терминале:
|
||||
|
||||
```sh
|
||||
brew tap hashicorp/tap
|
||||
brew install hashicorp/tap/terraform
|
||||
```
|
||||
|
||||
== Windows
|
||||
Установить по ссылке [386 v1.9.6](https://releases.hashicorp.com/terraform/1.9.6/terraform_1.9.6_windows_386.zip) [AMD64 v1.9.6](https://releases.hashicorp.com/terraform/1.9.6/terraform_1.9.6_windows_amd64.zip)
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_affinity_groups"
|
||||
description: "Get information about multiple beecloud affinity groups"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_affinity_groups
|
||||
|
||||
Получение информации обо всех аффинити-группах проекта.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
Получение всех аффинити-групп проекта
|
||||
|
||||
```hcl
|
||||
data "beecloud_affinity_groups" "all" {}
|
||||
|
||||
output "beecloud_affinity_groups_all" {
|
||||
value = data.beecloud_affinity_groups.all
|
||||
}
|
||||
```
|
||||
|
||||
Дополнительная фильтрация по ID региона
|
||||
|
||||
```hcl
|
||||
data "beecloud_affinity_groups" "by_region" {
|
||||
region = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
}
|
||||
|
||||
output "beecloud_affinity_groups_by_region" {
|
||||
value = data.beecloud_affinity_groups.by_region
|
||||
}
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
- `id` - Проект в beecloud, к которому относятся сервера (ID/Slug).
|
||||
- `affinity_groups` - Список аффинити-групп.
|
||||
- `affinity_group_id` - ID в системе TF. Соответствует ID группы.
|
||||
- `project_id` - ID проекта.
|
||||
- `name` - Наименование сервера.
|
||||
- `policy` - Политика группы (affinity/soft-affinity/anti-affinity/soft-anti-affinity).
|
||||
- `region` - Данные о регионе.
|
||||
- `id` - ID региона
|
||||
- `slug` - Слаг региона.
|
||||
- `name` - Наименование региона.
|
||||
- `condition` - Состояние группы (статус) (готовность к использованию).
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_flavors"
|
||||
description: "Get information about multiple beecloud Flavors of project"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_flavors
|
||||
|
||||
Получение информации обо всех тарифах, доступных проекту.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_flavors" "all" {}
|
||||
|
||||
output "beecloud_flavors_all" {
|
||||
value = data.beecloud_flavors.all
|
||||
}
|
||||
```
|
||||
|
||||
## Список доступных тарифов
|
||||
|
||||
```hcl
|
||||
data "beecloud_flavors" "all_slug" {}
|
||||
|
||||
output "flavors_slug" {
|
||||
value = data.beecloud_flavors.all_slug.flavors[*].slug
|
||||
}
|
||||
```
|
||||
|
||||
## Варианты фильтрации
|
||||
|
||||
Поиск по CPU и RAM
|
||||
|
||||
> RAM указывается в Мегабайтах пример (2048 Мб = 2 Гб)
|
||||
|
||||
```hcl
|
||||
data "beecloud_flavors" "cpu2" {
|
||||
filters {
|
||||
vcpu = 2
|
||||
}
|
||||
}
|
||||
|
||||
output "flavors_cpu2" {
|
||||
value = data.beecloud_flavors.cpu2
|
||||
}
|
||||
|
||||
data "beecloud_flavors" "cpu2ram2" {
|
||||
filters {
|
||||
vcpu = 2
|
||||
ram = 2 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
output "flavors_cpu2ram2" {
|
||||
value = data.beecloud_flavors.cpu2ram2
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - TF ID. Соответствует ID/Slug проекта.
|
||||
- `filters` - параметры фильтра
|
||||
- `ram` - значение заданного фильтра для ОЗУ
|
||||
- `vcpu` - значение заданного фильтра для ЦПУ
|
||||
- `flavors` - Список доступных тарифов.
|
||||
- `flavor_id` - ID тарифа.
|
||||
- `group` - Тарифная группа.
|
||||
- `name` - Наименование тарифа.
|
||||
- `ram` - Параметр ram тарифа.
|
||||
- `regions` - Доступен для регионов. Список.
|
||||
- `slug` - Slug тарифа.
|
||||
- `vcpu` - Параметр vcpu тарифа.
|
||||
|
||||
# beecloud_flavor
|
||||
|
||||
Получение информации о тарифе
|
||||
|
||||
```hcl
|
||||
data "beecloud_flavor" "cpu2_ram2" {
|
||||
slug = "cpu2ram2"
|
||||
}
|
||||
|
||||
output "flavor_id" {
|
||||
value = data.beecloud_flavor.cpu2_ram2.flavor_id
|
||||
}
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `flavor_id` - ID тарифа.
|
||||
- `slug` - Slug тарифа.
|
||||
- `name` - Наименование тарифа.
|
||||
- `vcpu` - Параметр vcpu тарифа.
|
||||
- `ram` - Параметр ram тарифа.
|
||||
- `group` - Тарифная группа.
|
||||
- `regions` - Доступен для регионов. Список.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_images"
|
||||
description: "Get information about multiple beecloud Images of project"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_images
|
||||
|
||||
Получение информации обо всех образах, доступных проекту.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_images" "all" {}
|
||||
|
||||
output "beecloud_images_all" {
|
||||
value = data.beecloud_images.all
|
||||
}
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - TF ID. Соответствует ID/Slug проекта.
|
||||
- `images` - Список доступных образов.
|
||||
- `image_id` - ID образа.
|
||||
- `slug` - Slug образа.
|
||||
- `name` - Наименование образа.
|
||||
- `distribution` - Дистрибутив образа.
|
||||
- `version` - Версия образа.
|
||||
- `min_disk` - Минимальный размер диска.
|
||||
- `regions` - Доступен для регионов. Список.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_regions"
|
||||
description: "Get information about multiple beecloud Regions of project"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_regions
|
||||
|
||||
Получение информации обо всех регионах, доступных проекту.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_regions" "all" {}
|
||||
|
||||
output "beecloud_regions_all" {
|
||||
value = data.beecloud_regions.all
|
||||
}
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - TF ID. Соответствует ID/Slug проекта.
|
||||
- `regions` - Список доступных регионов.
|
||||
- `region_id` - ID региона.
|
||||
- `slug` - Slug региона.
|
||||
- `name` - Наименование региона.
|
||||
- `hypervisor` - Гипервизор.
|
||||
- `location` - Местонахождение.
|
||||
- `priority` - Приоритет.
|
||||
- `zone` - Зона региона.
|
||||
- `volumes_type` - Доступные типы дисков. Список.
|
||||
- `features` - Сетевой периметр (?)
|
||||
- `limits` - Лимиты региона.
|
||||
- `instance` - Количество инстансов.
|
||||
- `ram` - Ограничение по RAM.
|
||||
- `vcpu` - Ограничение по VCPU.
|
||||
- `volumes_size` - Объект типа "key:value". Размеры для каждого типа диска (hhd/ssd/nvme/..)
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_server"
|
||||
description: "Get information about beecloud Instance"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_server
|
||||
|
||||
Получение информации о сервере.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_server" "server_by_id" {
|
||||
server_id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
}
|
||||
|
||||
output "server_beecloud" {
|
||||
value = data.beecloud_server.server_by_id
|
||||
}
|
||||
```
|
||||
|
||||
## Аргументы
|
||||
|
||||
- `server_id` - ID сервера.
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - ID TF. Соответствует ID сервера.
|
||||
- `name` - Наименование сервера.
|
||||
- `image` - Параметры образа сервера.
|
||||
- `id` - ID образа сервера.
|
||||
- `slug` - Slug образа.
|
||||
- `name` - Наименование образа.
|
||||
- `region` - Регион, где создан сервер.
|
||||
- `id` - ID региона.
|
||||
- `slug` - Slug региона.
|
||||
- `name` - Наименование региона.
|
||||
- `flavor` - Тариф.
|
||||
- `id` - ID тарифа.
|
||||
- `slug` - Slug тарифа.
|
||||
- `name` - Наименование тарифа.
|
||||
- `volumes` - Список дисков.
|
||||
- `id` - ID диска.
|
||||
- `size` - Размер диска.
|
||||
- `type` - Тип диска (hhd/ssd/nvme).
|
||||
- `is_boot` - Загрузочный/дополнительный (`true`, `false`).
|
||||
- `name` - Наименование диска.
|
||||
- `addresses` - Список адресов.
|
||||
- `type` - Тип адреса.
|
||||
- `address` - IP адрес.
|
||||
- `version` - Версия IP адреса (4/6).
|
||||
- `status` - Статус сервера (Создан/Создается/...)
|
||||
- `fqdn` - Полный домен.
|
||||
- `tags` - Список тэгов.
|
||||
- `wait_completed` - Ключ ожидания состояния сервера (Создан/Создается...).
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_servers"
|
||||
description: "Get information about multiple beecloud Instances of project"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_servers
|
||||
|
||||
Получение информации обо всех серверах проекта.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_servers" "all" {}
|
||||
|
||||
output "beecloud_servers_all" {
|
||||
value = data.beecloud_servers.all
|
||||
}
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - Проект в beecloud, к которому относятся серверыы (ID/Slug).
|
||||
- `servers` - Список серверов.
|
||||
- `server_id` - ID сервера.
|
||||
- `name` - Наименование сервера.
|
||||
- `image` - Параметры образа сервера.
|
||||
- `id` - ID образа сервера.
|
||||
- `slug` - Slug образа.
|
||||
- `name` - Наименование образа.
|
||||
- `region` - Регион, где создан сервер.
|
||||
- `id` - ID региона.
|
||||
- `slug` - Slug региона.
|
||||
- `name` - Наименование региона.
|
||||
- `flavor` - Тариф.
|
||||
- `id` - ID тарифа.
|
||||
- `slug` - Slug тарифа.
|
||||
- `name` - Наименование тарифа.
|
||||
- `volumes` - Список дисков.
|
||||
- `id` - ID диска.
|
||||
- `size` - Размер диска.
|
||||
- `type` - Тип диска (hhd/ssd/nvme).
|
||||
- `is_boot` - Загрузочный/дополнительный (`true`, `false`).
|
||||
- `name` - Наименование диска.
|
||||
- `addresses` - Список адресов.
|
||||
- `type` - Тип адреса.
|
||||
- `address` - IP адрес.
|
||||
- `version` - Версия IP адреса (4/6).
|
||||
- `status` - Статус сервера (Создан/Создается/...)
|
||||
- `fqdn` - Полный домен.
|
||||
- `tags` - Список тэгов.
|
||||
- `wait_completed` - Ключ ожидания состояния сервера (Создан/Создается...).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_volume"
|
||||
description: "Get information about beecloud Volume"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_volume
|
||||
Получение информации о диске.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_volume" "vlm_by_id" {
|
||||
volume_id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
}
|
||||
|
||||
output "beecloud_volume_by_id" {
|
||||
value = data.beecloud_volume.vlm_by_id
|
||||
}
|
||||
```
|
||||
|
||||
## Аргументы
|
||||
- `volume_id` - ID диска.
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - ID TF. Соответствует ID диска.
|
||||
- `project_id` - ID проекта.
|
||||
- `region_id` - ID региона.
|
||||
- `name` - Наименование диска.
|
||||
- `size` - Размер диска.
|
||||
- `type` - Тип диска.
|
||||
- `is_boot` - Загрузочный/дополнительный (`true`, `false`).
|
||||
- `attached_to` - ID сервера, к которому примонтирован.
|
||||
- `last_updated` - Время последнего обновления ресурса.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
page_title: "beecloud: beecloud_volumes"
|
||||
description: "Get information about multiple beecloud Volumes of project"
|
||||
---
|
||||
|
||||
# Источник данных: beecloud_volumes
|
||||
Получение информации обо всех дисках проекта.
|
||||
|
||||
## Пример конфигурации
|
||||
|
||||
```hcl
|
||||
data "beecloud_volumes" "all" {}
|
||||
|
||||
output "beecloud_volumes_all" {
|
||||
value = data.beecloud_volumes.all
|
||||
}
|
||||
```
|
||||
|
||||
## Возвращаемые поля
|
||||
|
||||
- `id` - ID TF. Соответствует ID/Slug проекта.
|
||||
- `volumes` - Список дисков.
|
||||
- `volume_id` - ID диска.
|
||||
- `project_id` - ID проекта.
|
||||
- `region_id` - ID региона.
|
||||
- `name` - Наименование диска.
|
||||
- `size` - Размер диска.
|
||||
- `type` - Тип диска.
|
||||
- `is_boot` - Загрузочный/дополнительный (`true`, `false`).
|
||||
- `attached_to` - ID сервера, к которому примонтирован.
|
||||
- `last_updated` - Время последнего обновления ресурса.
|
||||