Refine connectivity diagnostics service management
This commit is contained in:
@@ -1,23 +1,14 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { api } from '../api.js';
|
||||
import {
|
||||
assessConnectivity,
|
||||
CONNECTIVITY_IP_SOURCES,
|
||||
CONNECTIVITY_SITES,
|
||||
MAX_CUSTOM_DIAGNOSTIC_SERVICES,
|
||||
} from '../../shared/connectivityDiagnostics.js';
|
||||
|
||||
const CUSTOM_SERVICES_KEY = 'harbor-diagnostic-services';
|
||||
const SUMMARY_COPY = {
|
||||
available: ['is-good', 'Оба маршрута работают.'],
|
||||
'likely-direct-restriction': ['is-warning', 'Прямой маршрут, похоже, ограничен.'],
|
||||
'vpn-problem': ['is-error', 'VPN-маршрут требует проверки.'],
|
||||
'direct-offline': ['is-error', 'Прямой маршрут недоступен.'],
|
||||
offline: ['is-error', 'Оба маршрута недоступны.'],
|
||||
'same-ip': ['is-warning', 'Внешний IP не изменился.'],
|
||||
'vpn-off': ['is-muted', 'VPN выключен.'],
|
||||
inconclusive: ['is-muted', 'Недостаточно данных.'],
|
||||
};
|
||||
const HIDDEN_SERVICES_KEY = 'harbor-hidden-diagnostic-services';
|
||||
|
||||
function readCustomServices() {
|
||||
try {
|
||||
@@ -36,6 +27,17 @@ function readCustomServices() {
|
||||
}
|
||||
}
|
||||
|
||||
function readHiddenServices() {
|
||||
try {
|
||||
const value = JSON.parse(localStorage.getItem(HIDDEN_SERVICES_KEY) || '[]');
|
||||
return Array.isArray(value)
|
||||
? value.filter((id) => CONNECTIVITY_SITES.some((service) => service.id === id))
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function resultStatus(site, pending, available = true) {
|
||||
if (!available) return ['is-muted', '—'];
|
||||
if (pending) return ['is-running', 'Тестируем'];
|
||||
@@ -101,7 +103,7 @@ function mergePath(previous, incoming) {
|
||||
function mergeResult(previous, incoming) {
|
||||
const direct = mergePath(previous?.direct, incoming.direct);
|
||||
const vpn = mergePath(previous?.vpn, incoming.vpn);
|
||||
return { ...incoming, direct, vpn, assessment: assessConnectivity(direct, vpn) };
|
||||
return { ...incoming, direct, vpn };
|
||||
}
|
||||
|
||||
export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeRef, onClose }) {
|
||||
@@ -110,7 +112,9 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
const [activeTarget, setActiveTarget] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [customServices, setCustomServices] = useState(readCustomServices);
|
||||
const [hiddenServiceIds, setHiddenServiceIds] = useState(readHiddenServices);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [removingServiceId, setRemovingServiceId] = useState('');
|
||||
const [serviceName, setServiceName] = useState('');
|
||||
const [serviceUrl, setServiceUrl] = useState('');
|
||||
const [formError, setFormError] = useState('');
|
||||
@@ -126,6 +130,14 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
}
|
||||
}, [customServices]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(HIDDEN_SERVICES_KEY, JSON.stringify(hiddenServiceIds));
|
||||
} catch {
|
||||
// The service list still works for this session when browser storage is unavailable.
|
||||
}
|
||||
}, [hiddenServiceIds]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const sheet = sheetRef.current;
|
||||
const runner = runnerRef.current;
|
||||
@@ -157,7 +169,7 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
let next = result;
|
||||
const targets = [
|
||||
...CONNECTIVITY_IP_SOURCES.map(({ id }) => `ip:${id}`),
|
||||
...[...CONNECTIVITY_SITES, ...customServices].map(({ id }) => `site:${id}`),
|
||||
...sites.map(({ id }) => `site:${id}`),
|
||||
];
|
||||
for (const target of targets) {
|
||||
setActiveTarget(target);
|
||||
@@ -191,17 +203,49 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
setServiceUrl('');
|
||||
setFormError('');
|
||||
setAdding(false);
|
||||
setResult(null);
|
||||
} catch (validationError) {
|
||||
setFormError(validationError.message || 'Проверьте адрес.');
|
||||
}
|
||||
}
|
||||
|
||||
function removeService(serviceId) {
|
||||
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
finishRemoveService(serviceId);
|
||||
return;
|
||||
}
|
||||
setRemovingServiceId(serviceId);
|
||||
}
|
||||
|
||||
function finishRemoveService(serviceId) {
|
||||
const update = () => flushSync(() => {
|
||||
if (serviceId === 'draft') {
|
||||
setAdding(false);
|
||||
setServiceName('');
|
||||
setServiceUrl('');
|
||||
setFormError('');
|
||||
} else if (serviceId.startsWith('custom-')) {
|
||||
setCustomServices((services) => services.filter((service) => service.id !== serviceId));
|
||||
} else {
|
||||
setHiddenServiceIds((ids) => [...new Set([...ids, serviceId])]);
|
||||
}
|
||||
setRemovingServiceId('');
|
||||
setResult(null);
|
||||
});
|
||||
if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
document.startViewTransition(update);
|
||||
}
|
||||
|
||||
const pending = status === 'running';
|
||||
const sites = [...CONNECTIVITY_SITES, ...customServices];
|
||||
const summary = SUMMARY_COPY[result?.assessment?.summary] || SUMMARY_COPY.inconclusive;
|
||||
const checkedAt = result?.checkedAt
|
||||
? new Date(result.checkedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
: null;
|
||||
const sites = [
|
||||
...CONNECTIVITY_SITES.filter(({ id }) => !hiddenServiceIds.includes(id)),
|
||||
...customServices,
|
||||
];
|
||||
const serviceEditorBlocked = pending || Boolean(removingServiceId);
|
||||
const addHint = formError || (adding ? 'Введите адрес и нажмите «Добавить»' : '');
|
||||
|
||||
return (
|
||||
<aside
|
||||
@@ -243,14 +287,11 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{(error || result) && <div className="client-diagnostics-feedback" aria-live="polite">
|
||||
{error ? <div className="client-diagnostics-error" role="alert">
|
||||
{error && <div className="client-diagnostics-feedback">
|
||||
<div className="client-diagnostics-error" role="alert">
|
||||
<span>{error.message}</span>
|
||||
{error.retryable && <button type="button" onClick={run}>Повторить</button>}
|
||||
</div> : result && <>
|
||||
<strong className={`client-diagnostics-summary ${summary[0]}`}>{summary[1]}</strong>
|
||||
<span className="client-diagnostics-time">{checkedAt}</span>
|
||||
</>}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<section className="client-diagnostics-section" aria-labelledby="diagnostic-ip-title">
|
||||
@@ -277,16 +318,52 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
<section className="client-diagnostics-section" aria-labelledby="diagnostic-sites-title">
|
||||
<div className="client-diagnostics-section-title">
|
||||
<span id="diagnostic-sites-title">Сервисы</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending || customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES}
|
||||
onClick={() => setAdding((value) => !value)}
|
||||
>
|
||||
{customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES ? 'Лимит 5' : 'Добавить свой сервис'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{adding && <form className="client-diagnostics-add" onSubmit={addService}>
|
||||
<div className="client-diagnostics-service-table" role="table" aria-busy={pending}>
|
||||
<div className="client-diagnostics-service-header" role="row">
|
||||
<span role="columnheader">Сервис</span>
|
||||
<span role="columnheader">Напрямую</span>
|
||||
<span role="columnheader">VPN</span>
|
||||
<span aria-hidden="true" />
|
||||
</div>
|
||||
{sites.map((site) => {
|
||||
const direct = result?.direct?.sites?.find((item) => item.id === site.id);
|
||||
const vpn = result?.vpn?.sites?.find((item) => item.id === site.id);
|
||||
const running = activeTarget === `site:${site.id}`;
|
||||
const removing = removingServiceId === site.id;
|
||||
return <div
|
||||
key={site.id}
|
||||
role="row"
|
||||
data-diagnostic-target={`site:${site.id}`}
|
||||
className={`client-diagnostics-service-row client-deletable-row${running ? ' is-running' : ''}${removing ? ' is-removing' : ''}`}
|
||||
style={{ viewTransitionName: removing ? 'none' : `diagnostic-service-${site.id}` }}
|
||||
inert={removing ? true : undefined}
|
||||
>
|
||||
<span role="rowheader" className="client-diagnostics-service-name">{site.label}</span>
|
||||
<span role="cell"><Status value={resultStatus(direct, running)} route={`Напрямую, ${site.label}`} /></span>
|
||||
<span role="cell"><Status value={resultStatus(vpn, running, result?.vpn?.available !== false)} route={`VPN, ${site.label}`} /></span>
|
||||
<button
|
||||
className="client-local-rule-delete"
|
||||
type="button"
|
||||
aria-label={`Удалить сервис ${site.label}`}
|
||||
disabled={serviceEditorBlocked}
|
||||
onClick={() => removeService(site.id)}
|
||||
>×</button>
|
||||
<span
|
||||
className="client-delete-strike"
|
||||
aria-hidden="true"
|
||||
onAnimationEnd={() => finishRemoveService(site.id)}
|
||||
/>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{adding && <form
|
||||
className={`client-diagnostics-service-row client-diagnostics-service-draft client-deletable-row${removingServiceId === 'draft' ? ' is-removing' : ''}`}
|
||||
onSubmit={addService}
|
||||
inert={serviceEditorBlocked ? true : undefined}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
maxLength="40"
|
||||
@@ -295,51 +372,49 @@ export function ConnectivityDiagnosticsPanel({ isGateway, open, panelRef, closeR
|
||||
value={serviceName}
|
||||
onChange={(event) => setServiceName(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
inputMode="url"
|
||||
placeholder="https://example.com"
|
||||
aria-label="HTTPS-адрес сервиса"
|
||||
required
|
||||
value={serviceUrl}
|
||||
onChange={(event) => {
|
||||
setServiceUrl(event.target.value);
|
||||
setFormError('');
|
||||
}}
|
||||
<span className="client-diagnostics-service-url">
|
||||
<input
|
||||
type="url"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
placeholder="https://example.com"
|
||||
aria-label="HTTPS-адрес сервиса"
|
||||
required
|
||||
value={serviceUrl}
|
||||
onChange={(event) => {
|
||||
setServiceUrl(event.target.value);
|
||||
setFormError('');
|
||||
}}
|
||||
/>
|
||||
<button type="submit" disabled={serviceEditorBlocked}>Добавить</button>
|
||||
</span>
|
||||
<button
|
||||
className="client-local-rule-delete"
|
||||
type="button"
|
||||
aria-label="Отменить добавление сервиса"
|
||||
onClick={() => removeService('draft')}
|
||||
>×</button>
|
||||
<span
|
||||
className="client-delete-strike"
|
||||
aria-hidden="true"
|
||||
onAnimationEnd={() => finishRemoveService('draft')}
|
||||
/>
|
||||
<div className="client-diagnostics-add-action">
|
||||
<span role="alert">{formError}</span>
|
||||
<button type="submit">Добавить</button>
|
||||
</div>
|
||||
</form>}
|
||||
|
||||
<table className="client-diagnostics-table" aria-busy={pending}>
|
||||
<thead><tr>
|
||||
<th>Сервис</th>
|
||||
<th>Напрямую</th>
|
||||
<th>VPN</th>
|
||||
</tr></thead>
|
||||
<tbody>{sites.map((site) => {
|
||||
const direct = result?.direct?.sites?.find((item) => item.id === site.id);
|
||||
const vpn = result?.vpn?.sites?.find((item) => item.id === site.id);
|
||||
const custom = site.id.startsWith('custom-');
|
||||
const running = activeTarget === `site:${site.id}`;
|
||||
return <tr key={site.id} data-diagnostic-target={`site:${site.id}`} className={running ? 'is-running' : undefined}>
|
||||
<th scope="row">
|
||||
<span className="client-diagnostics-service-name">{site.label}</span>
|
||||
{custom && <button
|
||||
className="client-diagnostics-remove"
|
||||
type="button"
|
||||
aria-label={`Удалить сервис ${site.label}`}
|
||||
disabled={pending}
|
||||
onClick={() => setCustomServices((items) => items.filter((item) => item.id !== site.id))}
|
||||
>×</button>}
|
||||
</th>
|
||||
<td><Status value={resultStatus(direct, running)} route={`Напрямую, ${site.label}`} /></td>
|
||||
<td><Status value={resultStatus(vpn, running, result?.vpn?.available !== false)} route={`VPN, ${site.label}`} /></td>
|
||||
</tr>;
|
||||
})}</tbody>
|
||||
</table>
|
||||
{!sites.length && !adding && <p className="client-diagnostics-services-empty">Сервисов пока нет.</p>}
|
||||
|
||||
<div className="client-local-rule-add-slot client-diagnostics-add-slot">
|
||||
<button
|
||||
className="client-local-rule-add"
|
||||
type="button"
|
||||
disabled={serviceEditorBlocked || adding || customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES}
|
||||
onClick={() => setAdding(true)}
|
||||
>
|
||||
{customServices.length >= MAX_CUSTOM_DIAGNOSTIC_SERVICES ? 'Лимит 5 сервисов' : '+ Добавить сервис'}
|
||||
</button>
|
||||
<span className={addHint ? 'is-visible' : ''} role={formError ? 'alert' : 'status'}>{addHint}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
+123
-60
@@ -4675,9 +4675,7 @@ p {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.client-diagnostics-section-title button,
|
||||
.client-diagnostics-add button,
|
||||
.client-diagnostics-remove {
|
||||
.client-diagnostics-section-title button {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@@ -4693,7 +4691,6 @@ p {
|
||||
}
|
||||
|
||||
.client-diagnostics-section-title button:focus-visible,
|
||||
.client-diagnostics-add button:focus-visible,
|
||||
.client-diagnostics-error button:focus-visible {
|
||||
outline: 2px solid var(--client-accent);
|
||||
outline-offset: 2px;
|
||||
@@ -4774,6 +4771,60 @@ p {
|
||||
transition: color 420ms ease, text-shadow 520ms ease;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-table {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-header,
|
||||
.client-diagnostics-service-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 35% minmax(0, 1fr) minmax(0, 1fr) 28px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-header {
|
||||
padding: 2px 0 7px;
|
||||
color: var(--client-muted);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-header > span,
|
||||
.client-diagnostics-service-row > span:not(.client-delete-strike),
|
||||
.client-diagnostics-service-row > input {
|
||||
min-width: 0;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-row {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-height: 37px;
|
||||
border-top: 1px solid color-mix(in oklch, var(--client-border) 48%, transparent);
|
||||
animation: client-local-rule-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-row.is-removing {
|
||||
pointer-events: none;
|
||||
animation: client-local-rule-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-row.is-running .client-diagnostics-service-name {
|
||||
color: var(--client-accent);
|
||||
text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 38%, transparent);
|
||||
}
|
||||
|
||||
.client-diagnostics-service-name {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--client-text);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
transition: color 420ms ease, text-shadow 520ms ease;
|
||||
}
|
||||
|
||||
.client-diagnostics-table code,
|
||||
.client-diagnostics-status {
|
||||
display: inline-block;
|
||||
@@ -4783,23 +4834,19 @@ p {
|
||||
font: 600 9px/1.4 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
.client-diagnostics-status.is-good,
|
||||
.client-diagnostics-summary.is-good {
|
||||
.client-diagnostics-status.is-good {
|
||||
color: var(--client-accent);
|
||||
}
|
||||
|
||||
.client-diagnostics-status.is-warning,
|
||||
.client-diagnostics-summary.is-warning {
|
||||
.client-diagnostics-status.is-warning {
|
||||
color: oklch(0.68 0.14 72);
|
||||
}
|
||||
|
||||
.client-diagnostics-status.is-error,
|
||||
.client-diagnostics-summary.is-error {
|
||||
.client-diagnostics-status.is-error {
|
||||
color: oklch(0.68 0.15 28);
|
||||
}
|
||||
|
||||
.client-diagnostics-status.is-muted,
|
||||
.client-diagnostics-summary.is-muted {
|
||||
.client-diagnostics-status.is-muted {
|
||||
color: var(--client-muted);
|
||||
}
|
||||
|
||||
@@ -4830,71 +4877,65 @@ p {
|
||||
100% { clip-path: inset(0 100% 0 0); }
|
||||
}
|
||||
|
||||
.client-diagnostics-summary {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.client-diagnostics-time {
|
||||
color: var(--client-muted);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.client-diagnostics-add {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(100px, 0.7fr) minmax(0, 1.3fr);
|
||||
gap: 8px 14px;
|
||||
padding: 4px 8px 10px;
|
||||
}
|
||||
|
||||
.client-diagnostics-add input {
|
||||
.client-diagnostics-service-draft input {
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
padding: 0 2px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--client-border);
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--client-text);
|
||||
font: 500 9px/1 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||
box-shadow: 0 1px 0 transparent;
|
||||
transition: box-shadow 300ms ease, text-shadow 300ms ease;
|
||||
}
|
||||
|
||||
.client-diagnostics-add input:focus {
|
||||
border-color: var(--client-accent);
|
||||
.client-diagnostics-service-draft input::placeholder {
|
||||
color: color-mix(in oklch, var(--client-muted) 70%, transparent);
|
||||
}
|
||||
|
||||
.client-diagnostics-add-action {
|
||||
min-height: 20px;
|
||||
grid-column: 1 / -1;
|
||||
.client-diagnostics-service-draft input:focus {
|
||||
box-shadow: 0 1px 0 color-mix(in oklch, var(--client-accent) 58%, transparent);
|
||||
text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 22%, transparent);
|
||||
}
|
||||
|
||||
.client-diagnostics-service-url {
|
||||
min-width: 0;
|
||||
grid-column: 2 / 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-diagnostics-add-action span {
|
||||
color: oklch(0.68 0.15 28);
|
||||
font-size: 8px;
|
||||
.client-diagnostics-service-url input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-name {
|
||||
display: inline;
|
||||
.client-diagnostics-service-url button {
|
||||
padding: 5px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--client-accent);
|
||||
font: 700 8px/1.2 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, monospace;
|
||||
cursor: pointer;
|
||||
transition: filter 260ms ease, text-shadow 300ms ease;
|
||||
}
|
||||
|
||||
.client-diagnostics-remove {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: 3px;
|
||||
color: var(--client-muted);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.client-diagnostics-remove:hover,
|
||||
.client-diagnostics-remove:focus-visible {
|
||||
.client-diagnostics-service-url button:hover,
|
||||
.client-diagnostics-service-url button:focus-visible {
|
||||
outline: 0;
|
||||
color: oklch(0.68 0.15 28);
|
||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 42%, transparent));
|
||||
text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 42%, transparent);
|
||||
}
|
||||
|
||||
.client-diagnostics-services-empty {
|
||||
padding: 14px 8px 4px;
|
||||
color: var(--client-muted);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.client-diagnostics-add-slot {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
@@ -4907,13 +4948,31 @@ p {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
.client-diagnostics-add {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
.client-diagnostics-service-header,
|
||||
.client-diagnostics-service-row {
|
||||
grid-template-columns: 32% minmax(0, 1fr) minmax(0, 1fr) 44px;
|
||||
}
|
||||
|
||||
.client-diagnostics-add-action {
|
||||
.client-diagnostics-service-draft {
|
||||
grid-template-columns: minmax(0, 1fr) 44px;
|
||||
padding-block: 5px;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-draft > input {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-url {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.client-diagnostics-service-draft > .client-local-rule-delete {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 3;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -4999,7 +5058,11 @@ p {
|
||||
.client-diagnostics-refresh svg,
|
||||
.client-diagnostics-table,
|
||||
.client-diagnostics-active-marker,
|
||||
.client-diagnostics-dots::after {
|
||||
.client-diagnostics-dots::after,
|
||||
.client-diagnostics-service-row,
|
||||
.client-diagnostics-service-name,
|
||||
.client-diagnostics-service-draft input,
|
||||
.client-diagnostics-service-url button {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user