Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cicd/massive-redirect/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1926,5 +1926,17 @@
{
"from": "https://www.azion.com/en/documentation/store/sql-database/quickstart-api/",
"moved": "https://www.azion.com/en/documentation/store/sql-database/quickstart/"
},
{
"from": "https://www.azion.com/en/documentation/platform/",
"moved": "https://www.azion.com/en/documentation/fundamentals/how-it-works/"
},
{
"from": "https://www.azion.com/en/documentation/support/http-error-status-codes/",
"moved": "https://www.azion.com/en/documentation/fundamentals/http-status-codes/"
},
{
"from": "https://www.azion.com/en/documentation/first-deploy/",
"moved": "https://www.azion.com/en/documentation/fundamentals/first-deploy/"
}
]
12 changes: 12 additions & 0 deletions cicd/massive-redirect/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -1950,5 +1950,17 @@
{
"from": "https://www.azion.com/pt-br/documentacao/produtos/devtools/",
"moved": "https://www.azion.com/pt-br/documentacao/devtools/"
},
{
"from": "https://www.azion.com/pt-br/documentacao/plataforma/",
"moved": "https://www.azion.com/pt-br/documentacao/fundamentos/como-funciona/"
},
{
"from": "https://www.azion.com/pt-br/documentacao/suporte/http-error-status-codes/",
"moved": "https://www.azion.com/pt-br/documentacao/fundamentos/codigos-de-status-http/"
},
{
"from": "https://www.azion.com/pt-br/documentacao/primeiro-deploy/",
"moved": "https://www.azion.com/pt-br/documentacao/fundamentos/primeiro-deploy/"
}
]
3 changes: 3 additions & 0 deletions scripts/eslint-rules/mdx-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const WRAPPER_ALLOWED = [
/^@aziontech\/icons$/, // the icon font side-effect import
/^\.\/[^/]+\.vue$/, // a sibling wrapper, relative
/^~\/components\/webkit\/[^/]+\.vue$/, // a sibling wrapper, aliased
// A copy module under src/data: structured content with no UI dependency, the same
// argument the MDX rule makes for .md partials.
/^~\/data\/[^/]+$/,
// The docs search is an Algolia client; the dialog around it is webkit. A functional
// dependency, not a UI one — the single named exception.
/^algoliasearch(\/.+)?$/,
Expand Down
187 changes: 187 additions & 0 deletions src/components/webkit/PlatformTopology.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<template>
<div
v-bind="$attrs"
data-doc-block
data-doc-chrome
:data-testid="testId"
class="rounded-(--shape-card) border border-(--border-default) bg-(--bg-surface-raised)"
>
<!-- The wrapper centers the diagram; the flow root keeps its own horizontal scroll when
the column is narrower than the diagram, as the Console's topology does. -->
<div class="@container flex w-full justify-center">
<div class="min-w-0 max-w-full">
<FlowRoot align="start" :aria-label="content.ariaLabel" :data-testid="`${testId}__flow`">
<!-- A column with one card is a bare node, so the list only holds a group where
cards really stack; a group between the list and its items is an ARIA gap
the flow component owns. -->
<template v-for="(column, columnIndex) in content.columns" :key="columnIndex">
<FlowParallel
v-if="column.length > 1"
align="start"
:data-testid="`${testId}__column-${columnIndex}`"
>
<PlatformTopologyNode
v-for="node in column"
:key="node.id"
v-model:open="openIds[node.id]"
:node="node"
:reference-label="content.referenceLabel"
:id-prefix="uid"
:data-testid="`${testId}__node-${node.id}`"
@node-toggle="onToggle"
/>
</FlowParallel>
<PlatformTopologyNode
v-else
v-model:open="openIds[column[0].id]"
:node="column[0]"
:reference-label="content.referenceLabel"
:id-prefix="uid"
:data-testid="`${testId}__node-${column[0].id}`"
@node-toggle="onToggle"
/>
</template>
<!-- The flow draws lines only between consecutive columns. A link between two
cards stacked in the same column is measured here and drawn in the flow's
own connector style, over the same container the flow paints in. -->
<svg
v-if="content.links.length"
ref="linksRef"
:viewBox="linksViewBox"
preserveAspectRatio="none"
fill="none"
aria-hidden="true"
class="pointer-events-none absolute inset-0 h-full w-full overflow-visible"
:data-testid="`${testId}__links`"
>
<path
v-for="(d, index) in linkPaths"
:key="index"
:d="d"
stroke-width="1"
stroke-dasharray="4 4"
class="animate-flow-dash stroke-(--accent) motion-reduce:animate-none"
/>
</svg>
</FlowRoot>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import FlowParallel from '@aziontech/webkit/flow-parallel';
import FlowRoot from '@aziontech/webkit/flow-root';
import { computed, onBeforeUnmount, onMounted, reactive, ref, useAttrs, useId } from 'vue';

import PlatformTopologyNode from './PlatformTopologyNode.vue';
import { platformTopology } from '~/data/platform-topology';
import type { TopologyNode, TopologyNodeId } from '~/data/platform-topology';
import type { Lang } from '~/data/docs-home/types';

defineOptions({ name: 'PlatformTopology', inheritAttrs: false });

interface Props {
/** Language of the copy; it also selects the permalinks the cards link to. */
lang?: Lang;
/** Ids of the cards whose body starts open; every card opens and closes on its own. */
defaultOpen?: TopologyNodeId[];
}

const props = withDefaults(defineProps<Props>(), {
lang: 'en',
defaultOpen: () => [],
});

const emit = defineEmits<{
/** A card header was activated; the subject is the card's node. */
'node-toggle': [event: MouseEvent, node: TopologyNode];
}>();

const attrs = useAttrs();
const uid = useId();

const testId = computed(() => (attrs['data-testid'] as string | undefined) ?? 'platform-topology');
const content = computed(() => platformTopology[props.lang]);

const openIds = reactive<Record<string, boolean>>(
Object.fromEntries(props.defaultOpen.map((id) => [id, true]))
);

function onToggle(event: MouseEvent, node: TopologyNode) {
emit('node-toggle', event, node);
}

/* Links between two cards of one column: a straight vertical line from the top edge of the
lower card to the bottom edge of the upper one, measured in the flow container's space. The
svg stays mounted while the diagram declares a link, so a measurement can never unmount it. */
const linksRef = ref<SVGSVGElement | null>(null);
const linkPaths = ref<string[]>([]);
const linksViewBox = ref('0 0 0 0');
let observer: ResizeObserver | null = null;

function measureLinks() {
const svg = linksRef.value;
const container = svg?.parentElement;
if (!svg || !container) return;
const base = container.getBoundingClientRect();
const paths: string[] = [];
for (const link of content.value.links) {
const from = container.querySelector<HTMLElement>(
`[data-testid="${testId.value}__node-${link.from}"]`
);
const to = container.querySelector<HTMLElement>(
`[data-testid="${testId.value}__node-${link.to}"]`
);
if (!from || !to) continue;
const a = from.getBoundingClientRect();
const b = to.getBoundingClientRect();
const x = a.left - base.left + a.width / 2;
paths.push(`M${x},${a.top - base.top} L${x},${b.bottom - base.top}`);
}
linksViewBox.value = `0 0 ${container.clientWidth} ${container.clientHeight}`;
linkPaths.value = paths;
}

/* The flow measures its own lines on container resizes. A card animating shut stops resizing
the container as soon as a taller column takes over, while the cards below it keep moving,
so the container alone is not enough: the cards at both ends of a link are observed too, and
the end of every transition triggers one more measurement. Flipping `data-flow-disabled` on
the overlay, which the flow watches, makes the flow re-measure its lines at that point. */
let nudge = 0;
function settle() {
measureLinks();
const svg = linksRef.value;
if (!svg) return;
nudge = (nudge + 1) % 2;
if (nudge) svg.setAttribute('data-flow-disabled', 'false');
else svg.removeAttribute('data-flow-disabled');
}

onMounted(() => {
globalThis.requestAnimationFrame(() => {
measureLinks();
const container = linksRef.value?.parentElement;
if (!container) return;
container.addEventListener('transitionend', settle);
if ('ResizeObserver' in globalThis) {
observer = new ResizeObserver(() => measureLinks());
observer.observe(container);
for (const link of content.value.links) {
for (const id of [link.from, link.to]) {
const el = container.querySelector<HTMLElement>(
`[data-testid="${testId.value}__node-${id}"]`
);
if (el) observer.observe(el);
}
}
}
});
});

onBeforeUnmount(() => {
linksRef.value?.parentElement?.removeEventListener('transitionend', settle);
observer?.disconnect();
observer = null;
});
</script>
109 changes: 109 additions & 0 deletions src/components/webkit/PlatformTopologyNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<template>
<FlowNode unstyled :terminal="node.terminal ?? false" :data-testid="testId">
<!-- The Console's topology card. The width is a quarter of the container less the flow's
padding and gaps, clamped to 132px (four columns fit the article) and 224px (the
Console's card), so it grows with the space and never forces a horizontal scroll. -->
<div
:data-state="open ? 'open' : 'closed'"
class="flex w-[clamp(8.25rem,calc((100cqw_-_2*var(--spacing-md)_-_3*var(--spacing-xl))/4),14rem)] flex-col rounded-(--shape-card) border border-(--border-default) bg-(--bg-surface) shadow-(--shadow-sm) transition-colors duration-moderate-01 ease-productive-entrance hover:border-(--border-strong) has-[:focus-visible]:border-(--border-strong) motion-reduce:transition-none data-[state=open]:border-(--primary)"
>
<!-- The anchor pins the connector ports to the header, so an expanding body never
moves a card's lines. -->
<FlowAnchor>
<button
:id="triggerId"
type="button"
:aria-expanded="open"
:aria-controls="bodyId"
:data-state="open ? 'open' : 'closed'"
class="group flex w-full items-center gap-(--spacing-xxs) rounded-t-(--shape-card) px-(--spacing-xxs) pt-(--spacing-xs) pb-(--spacing-xxs) text-left outline-none transition-colors duration-moderate-01 ease-productive-entrance hover:bg-(--bg-hover) focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-inset motion-reduce:transition-none"
@click="toggle"
>
<i
:class="node.icon"
class="text-label-sm leading-none text-(--text-muted)"
aria-hidden="true"
/>
<span class="min-w-0 flex-1 truncate text-label-sm text-(--text-muted)">{{
node.kind
}}</span>
<i
class="pi pi-chevron-down text-label-sm leading-none text-(--text-muted) transition-transform duration-moderate-01 ease-productive-entrance group-data-[state=open]:rotate-180 motion-reduce:transition-none"
aria-hidden="true"
/>
</button>
</FlowAnchor>
<p
class="m-0 truncate px-(--spacing-xs) pb-(--spacing-xs) text-label-sm text-(--text-default)"
>
{{ node.role }}
</p>
<div
:id="bodyId"
role="region"
:aria-labelledby="triggerId"
:inert="!open"
:data-state="open ? 'open' : 'closed'"
class="grid grid-rows-[0fr] transition-[grid-template-rows] duration-moderate-01 ease-productive-entrance motion-reduce:transition-none data-[state=open]:grid-rows-[1fr]"
>
<div class="overflow-hidden">
<div
class="flex flex-col items-start gap-(--spacing-xs) border-t border-(--border-muted) px-(--spacing-xs) py-(--spacing-xs)"
>
<Tag size="small" :severity="node.tag.severity" :label="node.tag.label" />
<ul v-if="node.items?.length" class="m-0 flex list-none flex-col p-0">
<li v-for="item in node.items" :key="item.id">
<Link :href="item.href" :label="item.label" size="small" :show-icon="false" />
</li>
</ul>
<Link :href="node.href" :label="referenceLabel" size="small" icon="pi pi-arrow-right" />
</div>
</div>
</div>
</div>
</FlowNode>
</template>

<script setup lang="ts">
import FlowAnchor from '@aziontech/webkit/flow-anchor';
import FlowNode from '@aziontech/webkit/flow-node';
import Link from '@aziontech/webkit/link';
import Tag from '@aziontech/webkit/tag';
import { computed, useAttrs } from 'vue';

import type { TopologyNode } from '~/data/platform-topology';

defineOptions({ name: 'PlatformTopologyNode', inheritAttrs: false });

interface Props {
/** The resource this card stands for, with what it holds. */
node: TopologyNode;
/** Label of the link that closes the body. */
referenceLabel: string;
/** Id prefix of the header and the body, so the pair stays unique per diagram. */
idPrefix: string;
}

const props = defineProps<Props>();

/** Whether the body is open; the diagram seeds it and every card toggles on its own. */
const open = defineModel<boolean>('open', { default: false });

const emit = defineEmits<{
/** The header was activated; the subject is the card's node. */
'node-toggle': [event: MouseEvent, node: TopologyNode];
}>();

const attrs = useAttrs();

const testId = computed(
() => (attrs['data-testid'] as string | undefined) ?? `platform-topology__node-${props.node.id}`
);
const triggerId = computed(() => `${props.idPrefix}-${props.node.id}-trigger`);
const bodyId = computed(() => `${props.idPrefix}-${props.node.id}-body`);

function toggle(event: MouseEvent) {
open.value = !open.value;
emit('node-toggle', event, props.node);
}
</script>
42 changes: 42 additions & 0 deletions src/components/webkit/UseCaseList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<!-- Plain list markup on purpose: it sits inside the prose, which styles lists and links,
so the rendered list matches the markdown lists around it. -->
<ul v-bind="$attrs" :data-testid="testId">
<li v-for="useCase in shown" :key="useCase.id">
<a v-if="useCase.href" :href="useCase.href">{{ useCase.title }}</a>
<template v-else>{{ useCase.title }}</template>
</li>
</ul>
</template>

<script setup lang="ts">
import { computed, useAttrs } from 'vue';

import type { Lang } from '~/data/docs-home/types';
import { useCaseCatalog } from '~/data/use-cases';
import type { UseCaseSolutionId } from '~/data/use-cases';

defineOptions({ name: 'UseCaseList', inheritAttrs: false });

interface Props {
/** The Solution whose use cases the list shows. */
solution: UseCaseSolutionId;
/** Language of the titles and the permalinks. */
lang?: Lang;
/** Only the featured selection, or every catalogued use case of the Solution. */
featured?: boolean;
}

const props = withDefaults(defineProps<Props>(), { lang: 'en', featured: true });

const attrs = useAttrs();

const testId = computed(
() => (attrs['data-testid'] as string | undefined) ?? `use-case-list-${props.solution}`
);
const shown = computed(() =>
useCaseCatalog[props.lang].useCases.filter(
(useCase) => useCase.solution === props.solution && (!props.featured || useCase.featured)
)
);
</script>
Loading
Loading