зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1435232 - Apply new status code design to details panel. r=Honza
This commit is contained in:
Родитель
201d19b399
Коммит
377b1b9417
|
@ -210,10 +210,6 @@
|
|||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
.network-monitor .headers-summary .requests-list-status-icon {
|
||||
min-width: 10px;
|
||||
}
|
||||
|
||||
.network-monitor .headers-summary .raw-headers-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
|
|
@ -27,11 +27,11 @@ const {
|
|||
|
||||
// Components
|
||||
const PropertiesView = createFactory(require("./PropertiesView"));
|
||||
const StatusCode = createFactory(require("./StatusCode"));
|
||||
|
||||
loader.lazyGetter(this, "MDNLink", function() {
|
||||
return createFactory(require("./MdnLink"));
|
||||
});
|
||||
|
||||
loader.lazyGetter(this, "Rep", function() {
|
||||
return require("devtools/client/shared/components/reps/reps").REPS.Rep;
|
||||
});
|
||||
|
@ -180,6 +180,7 @@ class HeadersPanel extends Component {
|
|||
urlDetails,
|
||||
},
|
||||
} = this.props;
|
||||
let item = { fromCache, fromServiceWorker, status, statusText };
|
||||
|
||||
if ((!requestHeaders || !requestHeaders.headers.length) &&
|
||||
(!uploadHeaders || !uploadHeaders.headers.length) &&
|
||||
|
@ -209,38 +210,17 @@ class HeadersPanel extends Component {
|
|||
let summaryStatus;
|
||||
|
||||
if (status) {
|
||||
let code;
|
||||
if (fromCache) {
|
||||
code = "cached";
|
||||
} else if (fromServiceWorker) {
|
||||
code = "service worker";
|
||||
} else {
|
||||
code = status;
|
||||
}
|
||||
|
||||
let statusCodeDocURL = getHTTPStatusCodeURL(status.toString());
|
||||
let inputWidth = status.toString().length + statusText.length + 1;
|
||||
let toggleRawHeadersClassList = ["devtools-button", "raw-headers-button"];
|
||||
if (this.state.rawHeadersOpened) {
|
||||
toggleRawHeadersClassList.push("checked");
|
||||
}
|
||||
|
||||
summaryStatus = (
|
||||
div({ className: "tabpanel-summary-container headers-summary" },
|
||||
div({
|
||||
className: "tabpanel-summary-label headers-summary-label",
|
||||
}, SUMMARY_STATUS),
|
||||
div({
|
||||
className: "requests-list-status-icon",
|
||||
"data-code": code,
|
||||
}),
|
||||
input({
|
||||
className: "tabpanel-summary-value textbox-input devtools-monospace"
|
||||
+ " status-text",
|
||||
readOnly: true,
|
||||
value: `${status} ${statusText}`,
|
||||
size: `${inputWidth}`,
|
||||
}),
|
||||
StatusCode({ item }),
|
||||
statusCodeDocURL ? MDNLink({
|
||||
url: statusCodeDocURL,
|
||||
}) : span({
|
||||
|
|
|
@ -4,21 +4,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { Component } = require("devtools/client/shared/vendor/react");
|
||||
const { Component, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const { L10N } = require("../utils/l10n");
|
||||
const { propertiesEqual } = require("../utils/request-utils");
|
||||
|
||||
// Components
|
||||
|
||||
const StatusCode = createFactory(require("./StatusCode"));
|
||||
|
||||
const { div } = dom;
|
||||
|
||||
const UPDATED_STATUS_PROPS = [
|
||||
"fromCache",
|
||||
"fromServiceWorker",
|
||||
"status",
|
||||
"statusText",
|
||||
];
|
||||
|
||||
class RequestListColumnStatus extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
|
@ -26,68 +21,16 @@ class RequestListColumnStatus extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { item } = this.props;
|
||||
let { fromCache, fromServiceWorker, status, statusText } = item;
|
||||
let code;
|
||||
|
||||
if (status) {
|
||||
if (fromCache) {
|
||||
code = "cached";
|
||||
} else if (fromServiceWorker) {
|
||||
code = "service worker";
|
||||
} else {
|
||||
code = status;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
div({
|
||||
className: "requests-list-column requests-list-status",
|
||||
onMouseOver: function({ target }) {
|
||||
if (status && statusText && !target.title) {
|
||||
target.title = getColumnTitle(item);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
`data-code` refers to the status-code
|
||||
`data-status-code` can be one of "cached", "service worker"
|
||||
or the status-code itself
|
||||
For example - if a resource is cached, `data-code` would be 200
|
||||
and the `data-status-code` would be "cached"
|
||||
*/
|
||||
div({
|
||||
className: "requests-list-status-code status-code",
|
||||
"data-status-code": code,
|
||||
"data-code": status,
|
||||
}, status)
|
||||
StatusCode({ item }),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
function getColumnTitle(item) {
|
||||
let { fromCache, fromServiceWorker, status, statusText } = item;
|
||||
let title;
|
||||
if (fromCache && fromServiceWorker) {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.cachedworker",
|
||||
status, statusText);
|
||||
} else if (fromCache) {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.cached",
|
||||
status, statusText);
|
||||
} else if (fromServiceWorker) {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.worker",
|
||||
status, statusText);
|
||||
} else {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.simple",
|
||||
status, statusText);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
module.exports = RequestListColumnStatus;
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Component } = require("devtools/client/shared/vendor/react");
|
||||
const dom = require("devtools/client/shared/vendor/react-dom-factories");
|
||||
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
||||
const { L10N } = require("../utils/l10n");
|
||||
const { propertiesEqual } = require("../utils/request-utils");
|
||||
|
||||
const { div } = dom;
|
||||
|
||||
const UPDATED_STATUS_PROPS = [
|
||||
"fromCache",
|
||||
"fromServiceWorker",
|
||||
"status",
|
||||
"statusText",
|
||||
];
|
||||
|
||||
/**
|
||||
* Status code component
|
||||
* Displays HTTP status code icon
|
||||
* Used in RequestListColumnStatus and HeadersPanel
|
||||
*/
|
||||
class StatusCode extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
item: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { item } = this.props;
|
||||
let { fromCache, fromServiceWorker, status, statusText } = item;
|
||||
let code;
|
||||
|
||||
if (status) {
|
||||
if (fromCache) {
|
||||
code = "cached";
|
||||
} else if (fromServiceWorker) {
|
||||
code = "service worker";
|
||||
} else {
|
||||
code = status;
|
||||
}
|
||||
}
|
||||
|
||||
// `data-code` refers to the status-code
|
||||
// `data-status-code` can be one of "cached", "service worker"
|
||||
// or the status-code itself
|
||||
// For example - if a resource is cached, `data-code` would be 200
|
||||
// and the `data-status-code` would be "cached"
|
||||
return (
|
||||
div({
|
||||
className: "requests-list-status-code status-code",
|
||||
onMouseOver: function({ target }) {
|
||||
if (status && statusText && !target.title) {
|
||||
target.title = getStatusTooltip(item);
|
||||
}
|
||||
},
|
||||
"data-status-code": code,
|
||||
"data-code": status,
|
||||
}, status)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusTooltip(item) {
|
||||
let { fromCache, fromServiceWorker, status, statusText } = item;
|
||||
let title;
|
||||
if (fromCache && fromServiceWorker) {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.cachedworker",
|
||||
status, statusText);
|
||||
} else if (fromCache) {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.cached",
|
||||
status, statusText);
|
||||
} else if (fromServiceWorker) {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.worker",
|
||||
status, statusText);
|
||||
} else {
|
||||
title = L10N.getFormatStr("netmonitor.status.tooltip.simple",
|
||||
status, statusText);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
module.exports = StatusCode;
|
|
@ -41,6 +41,7 @@ DevToolsModules(
|
|||
'StackTracePanel.js',
|
||||
'StatisticsPanel.js',
|
||||
'StatusBar.js',
|
||||
'StatusCode.js',
|
||||
'TabboxPanel.js',
|
||||
'TimingsPanel.js',
|
||||
'Toolbar.js',
|
||||
|
|
|
@ -29,7 +29,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, BROTLI_REQUESTS);
|
||||
|
||||
let requestItem = document.querySelector(".request-list-item");
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ add_task(async function() {
|
|||
for (let request of REQUEST_DATA) {
|
||||
let requestItem = document.querySelectorAll(".request-list-item")[index];
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, CONTENT_TYPE_WITHOUT_CACHE_REQUESTS);
|
||||
|
||||
for (let requestItem of document.querySelectorAll(".request-list-item")) {
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
requestItem.scrollIntoView();
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
|
|
@ -24,7 +24,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, 1);
|
||||
|
||||
let requestItem = document.querySelectorAll(".request-list-item")[0];
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
requestItem.scrollIntoView();
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
|
|
@ -23,7 +23,7 @@ add_task(async function() {
|
|||
await wait;
|
||||
|
||||
let requestItem = document.querySelectorAll(".request-list-item")[0];
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
requestItem.scrollIntoView();
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
|
|
@ -307,7 +307,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -209,7 +209,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -392,7 +392,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, 1);
|
||||
|
||||
let requestItem = document.querySelector(".request-list-item");
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, 1);
|
||||
|
||||
let requestItem = document.querySelector(".request-list-item");
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, 1);
|
||||
|
||||
let requestItem = document.querySelector(".request-list-item");
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ add_task(async function() {
|
|||
await performRequests(monitor, tab, 1);
|
||||
|
||||
let requestItem = document.querySelector(".request-list-item");
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ add_task(async function() {
|
|||
|
||||
let requestItem = document.querySelector(".request-list-item");
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -224,7 +224,7 @@ function test() {
|
|||
|
||||
let requestListItem = document.querySelector(".request-list-item");
|
||||
requestListItem.scrollIntoView();
|
||||
let requestsListStatus = requestListItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestListItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -243,7 +243,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ add_task(async function() {
|
|||
let requestListItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestListItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
@ -175,13 +175,16 @@ add_task(async function() {
|
|||
let panel = document.querySelector("#headers-panel");
|
||||
let summaryValues = panel.querySelectorAll(".tabpanel-summary-value.textbox-input");
|
||||
let { method, correctUri, details: { status, statusText } } = data;
|
||||
let statusCode = panel.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, statusCode);
|
||||
await waitUntil(() => statusCode.title);
|
||||
|
||||
is(summaryValues[0].value, correctUri,
|
||||
"The url summary value is incorrect.");
|
||||
is(summaryValues[1].value, method, "The method summary value is incorrect.");
|
||||
is(panel.querySelector(".requests-list-status-icon").dataset.code, status,
|
||||
is(statusCode.dataset.code, status,
|
||||
"The status summary code is incorrect.");
|
||||
is(summaryValues[3].value, status + " " + statusText,
|
||||
is(statusCode.getAttribute("title"), status + " " + statusText,
|
||||
"The status summary value is incorrect.");
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ add_task(async function() {
|
|||
let requestItems = document.querySelectorAll(".request-list-item");
|
||||
for (let requestItem of requestItems) {
|
||||
requestItem.scrollIntoView();
|
||||
let requestsListStatus = requestItem.querySelector(".requests-list-status");
|
||||
let requestsListStatus = requestItem.querySelector(".status-code");
|
||||
EventUtils.sendMouseEvent({ type: "mouseover" }, requestsListStatus);
|
||||
await waitUntil(() => requestsListStatus.title);
|
||||
}
|
||||
|
|
|
@ -493,7 +493,8 @@ function verifyRequestItemTarget(document, requestList, requestItem, method,
|
|||
let value = target.querySelector(".requests-list-status-code")
|
||||
.getAttribute("data-status-code");
|
||||
let codeValue = target.querySelector(".requests-list-status-code").textContent;
|
||||
let tooltip = target.querySelector(".requests-list-status").getAttribute("title");
|
||||
let tooltip = target.querySelector(".requests-list-status-code")
|
||||
.getAttribute("title");
|
||||
info("Displayed status: " + value);
|
||||
info("Displayed code: " + codeValue);
|
||||
info("Tooltip status: " + tooltip);
|
||||
|
|
Загрузка…
Ссылка в новой задаче