Skip to content

Соединения и правка ​

По умолчанию карта только читает. Эта страница — про то, что позволяет менять нарисованное на ней: виды соединений и их легенду, окно соединения, меню по правому клику, правку сущностей и связей и рисование новой связи между двумя узлами.

Через всё это проходит одно правило: карта никогда не пишет в entities и links. Она сообщает, о чём попросили, а решает приложение. Каждая нагрузка ниже — новый объект, применить его ваша задача. Именно это и позволяет держать карту поверх стора, API, стека отмены — или ничего.

Виды соединений ​

linkTypes объявляет виды, которые есть в ваших данных. Одно объявление управляет тремя вещами сразу, и в этом весь смысл: цвет в легенде всегда принадлежит связи, которую действительно можно нарисовать, а редактируемый тип никогда не оказывается тем, о котором легенда не знает.

vue
<script setup lang="ts">
import type { LinkTypeDef } from 'vue-asset-map'

const linkTypes: LinkTypeDef[] = [
  { id: 'fiber', label: 'Оптоволокно', color: '#2f7d4f', width: 2 },
  { id: 'wifi', label: 'Wi-Fi', color: '#c07b1e', dash: '2 3' },
  { id: 'vpn', label: 'VPN-туннель', color: '#5a5ee7', dash: '6 4' },
]
</script>

<template>
  <AssetMap :entities="entities" :links="links" :link-types="linkTypes" show-legend />
</template>

id сравнивается с link.type. Задавайте каждому виду не только color, но и dash: один цвет не переживает монохромную печать и красно-зелёную аномалию, а легенду должен читать каждый, кто видит карту.

Цвет вида применяется инлайном и намеренно не применяется, пока связь подсвечена, погашена или заражена. Инлайновый stroke побеждает любой класс, и, оставь мы его, эти состояния молча не отрисовались бы: в такие моменты то, чем связь является, менее важно, чем то, что с ней происходит.

Легенда ​

showLegend ставит её слева снизу — в последний угол, не занятый тулбаром и обзорной картой. В неё попадают только виды, которые рисуются по-особому: вид без цвета и штриха неотличим от дефолтного, а строка, которая ничего не объясняет, хуже её отсутствия. Каждый образец рисует настоящую линию — тот же цвет, тот же штрих, ту же толщину: легенде, которая лишь приблизительно похожа на карту, приходится верить на слово.

Окно соединения ​

Клик по связи открывает окно с тем, что она соединяет, её видом, слоями и meta. Если окну сущности для осмысленного содержимого нужен слот #details, то у связи форма достаточно фиксированная, чтобы пакет показал её сам.

Набор открытых окон — такое же состояние, как любое другое:

vue
<AssetMap v-model:open-links="openLinks" :entities="entities" :links="links" />

Агрегированное ребро не открывает ничего: за ним стоит сразу несколько связей и нет одной записи, которую можно было бы описать. Клик при этом по-прежнему приходит событием edge-click.

#link-details подменяет тело — так же, как #details у сущностей.

По одному окну за раз ​

Обычный клик теперь заменяет открытое, а не добавляет к нему: кликая по карте, читают по узлу за раз, а не закапывают её под окнами. detailsMode: 'stack' возвращает прежнее поведение.

В обоих режимах «Открыть в новом окне» из контекстного меню накапливает окна — оно ровно для этого и есть.

vue
<AssetMap :entities="entities" :links="links" details-mode="stack" />

Меню по правому клику ​

showContextMenu включает меню на узлах и на связях. На узле оно предлагает открыть сущность вторым окном, на связи — открыть её подробности. С editable оба добавляют удаление.

vue
<AssetMap :entities="entities" :links="links" show-context-menu editable />

По умолчанию выключено: своё меню всегда вешали через node-context-menu, и встроенное, всплывающее поверх него, стало бы неожиданностью для тех, кто так уже делает. Событие приходит в любом случае.

У группового узла меню нет: за ним не стоит сущности, к которой относился бы хоть один пункт.

Удаление ​

Удаление всегда проходит через подтверждение, и это подтверждение — собственное. Не window.confirm, который не стилизуется и выглядит как фишинг, и не библиотека диалогов, которая стала бы рантайм-зависимостью ради одного окошка. Оно собрано из тех же токенов, что и вся карта, поэтому следует теме. Фокус открывается на отмене: разрушительное действие не должно оказаться тем, на что попадёт случайный Enter.

Пока вы не удалите — ничего не удалено:

vue
<AssetMap
  :entities="entities"
  :links="links"
  show-context-menu
  editable
  @entity-delete="onEntityDelete"
  @link-delete="onLinkDelete"
/>
ts
function onEntityDelete({ id }: { id: string }): void {
  entities.value = entities.value.filter((entity) => entity.id !== id)
  // Связи узла уходят вместе с ним, иначе построитель графа сочтёт их висячими.
  links.value = links.value.filter((link) => link.source !== id && link.target !== id)
}

Перед событием карта закрывает окно удаляемого и убирает его из выделения: оставить окно открытым над записью, которую приложению только что велели убрать, значило бы показывать то, чего вот-вот не станет.

Правка ​

editable добавляет под тело окна кнопку «Редактировать». Первое нажатие превращает тело в форму, второе — сохраняет и возвращает обратно.

Схему можно не объявлять: карта предложит те два поля, значение которых знает сама — label и status выбором по тем статусам, что вы уже назвали в themeConfig.statusColors (свободный текст, если не назвали ни одного). У связей та же любезность, поэтому одного editable хватает, чтобы правились обе записи, а не одна из них молча нет.

Для всего, что лежит в meta, схема нужна: ни имён, ни того, какие из них важны, пакет не угадает.

Схема — это данные, потому что правка — единственное, чего пакет не может сделать на вашем словаре: он знает, что у сущности есть label и meta, но не знает, что meta.speed меряется в мегабитах и какие из ваших двадцати полей вообще стоит показывать человеку.

vue
<script setup lang="ts">
import type { FieldDef } from 'vue-asset-map'

const entityFields: FieldDef[] = [
  { key: 'label', label: 'Название' },
  {
    key: 'status',
    label: 'Состояние',
    type: 'select',
    options: [
      { value: 'online', label: 'В сети' },
      { value: 'offline', label: 'Не в сети' },
    ],
  },
  { key: 'meta.owner', label: 'Владелец' },
]
</script>

<template>
  <AssetMap
    :entities="entities"
    :links="links"
    :entity-fields="entityFields"
    editable
    @entity-update="({ id, entity }) => replaceEntity(id, entity)"
  />
</template>

key — точечный путь. Он читается для показа и записывается при сохранении, а недостающие ветки создаются: meta.speed работает и на сущности, у которой meta ещё нет. Каждый уровень пути копируется, а не мутируется.

Типы полей: 'text' (по умолчанию), 'number', 'select' и 'textarea'. Поле с readonly показывается, но не правится.

На что можно опираться:

  • В режиме правки форма занимает тело целиком, в том числе когда вы заполнили #details. В представлении для чтения печатать негде, поэтому потребитель со своим телом всё равно получает рабочую форму по объявленным полям, а не кнопку, которая обещает несбыточное.
  • Нетронутая форма ничего не эмитит. Открыть редактор и закрыть — не правка, до entity-update она не доходит.
  • Пустое поле означает «нет значения», а не пустую строку: для необязательного поля это разные виды неверного.
  • Неразобранное число в числовом поле сохраняет введённый текст. Молча превратить его в NaN значило бы уничтожить набранное.
  • Отмена выбрасывает черновик, как и замена записи под открытым окном.
  • В форме одно поле на строку, подпись сверху, контрол во всю ширину окна. Чтение — это сканирование, поэтому читающий вид остаётся двухколоночным; правка — это набор текста, а контрол, ужатый в половину панели, и есть то, из-за чего форма кажется сделанной впопыхах.

Свои контролы ​

#details-edit подменяет форму целиком — для оформления, которое ничем не обязано инпутам пакета. Слот получает запись, объявленные поля, черновик, функцию записи в него и два действия, которые выполняют встроенные кнопки:

vue
<template #details-edit="{ value, fields, values, setValue, save, cancel }">
  <MyForm
    :entity="value"
    :fields="fields"
    :draft="values"
    @change="setValue"
    @submit="save"
    @cancel="cancel"
  />
</template>

#link-edit делает то же для окон соединений. Ничего из переданного в эти слоты не является компонентом: форма, привязанная к контролам пакета, — ровно то, от чего эти слоты и позволяют уйти.

Связи устроены так же — через linkFields и link-update. Не объявляйте ничего, и при editable карта предложит разумный минимум: тип выбором по вашим linkTypes, плюс подпись и скорость. Это покрывает форму почти любой связи, и повторять её не приходится.

Рисование новой связи ​

С editable при наведении на узел по краям его круга появляются четыре точки захвата. Перетаскивание от одной из них к другому узлу создаёт кандидата в связи, и его окно открывается сразу в режиме правки, чтобы вид и скорость заполнялись, пока намерение ещё свежо.

Четыре, а не одна, потому что направление, в котором тянут, — это заявление о том, куда должна идти связь; одна ручка заставляла бы все связи выходить из одного и того же края круга. До наведения или фокуса точки скрыты: четыре кружка на каждом узле в покое — это четыре элемента интерфейса на узел на карте, рассчитанной на сотни.

Точки захвата объявлены после центральных ручек узла, и якорями нарисованных рёбер остаются именно центральные: ребро, не назвавшее ручку, разрешается в первую своего типа, поэтому появление точек не сдвигает ни одной линии. У групповых узлов их нет — у связи с группой не было бы сущности на одном из концов.

vue
<AssetMap :entities="entities" :links="links" :link-types="linkTypes" editable @link-create="onLinkCreate" />
ts
function onLinkCreate({ link }: { link: AssetLink }): void {
  links.value = [...links.value, link]
}

Кандидат получает слой текущего представления: связь, нарисованная в виде, в нём и видна — исчезнуть сразу после создания было бы здесь очевидной ловушкой.

Пока форму не сохранили, связи не существует. link-create приходит на первом сохранении, а не на перетаскивании; закрыть окно раньше — значит отказаться от кандидата совсем. Перетаскивание, закончившееся там же, где началось, связью не считается.

MIT licensed