mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-09 21:28:43 +08:00
Report Trust Token operation result in network panel
This CL adds the 'Results' portion of the Trust Token tab in the Network panel. Please note that the issuer and top-level origin are reported by the result event due to the way the backend is implemented. The two origins are considered part of the parameters though, and are displayed in the 'Parameters' section for that reason. Screenshots: https://imgur.com/a/XceM0Xl R=petermueller@chromium.org Bug: chromium:1126824 Change-Id: I23e6a5dcddb4be8ffbe95dc25bee75ffaedc2064 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2584968 Commit-Queue: Simon Zünd <szuend@chromium.org> Reviewed-by: Sigurd Schneider <sigurds@chromium.org>
This commit is contained in:
@@ -56,6 +56,7 @@ grd_files_release_sources = [
|
||||
"front_end/Images/flex-justify-content-space-around-icon.svg",
|
||||
"front_end/Images/flex-justify-content-space-between-icon.svg",
|
||||
"front_end/Images/flex-justify-content-space-evenly-icon.svg",
|
||||
"front_end/Images/ic_checkmark_16x16.svg",
|
||||
"front_end/Images/ic_delete_filter.svg",
|
||||
"front_end/Images/ic_delete_list.svg",
|
||||
"front_end/Images/ic_info_black_18dp.svg",
|
||||
|
||||
@@ -51,6 +51,7 @@ devtools_image_files = [
|
||||
"flex-align-self-flex-start-icon.svg",
|
||||
"flex-align-self-stretch-icon.svg",
|
||||
"baseline-icon.svg",
|
||||
"ic_checkmark_16x16.svg",
|
||||
"ic_info_black_18dp.svg",
|
||||
"ic_page_next_16x16_icon.svg",
|
||||
"ic_page_prev_16x16_icon.svg",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0.5C3.86 0.5 0.5 3.86 0.5 8C0.5 12.14 3.86 15.5 8 15.5C12.14 15.5 15.5 12.14 15.5 8C15.5 3.86 12.14 0.5 8 0.5ZM8 14C4.6925 14 2 11.3075 2 8C2 4.6925 4.6925 2 8 2C11.3075 2 14 4.6925 14 8C14 11.3075 11.3075 14 8 14ZM6.5 9.65L4.55 7.7L3.5 8.75L6.5 11.75L12.5 5.75L11.45 4.7L6.5 9.65Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
@@ -43,6 +43,7 @@ devtools_module("network") {
|
||||
"../components:bundle",
|
||||
"../cookie_table:bundle",
|
||||
"../data_grid:bundle",
|
||||
"../elements:bundle",
|
||||
"../har_importer:bundle",
|
||||
"../host:bundle",
|
||||
"../mobile_throttling:bundle",
|
||||
|
||||
@@ -65,6 +65,10 @@ export class RequestTrustTokensReport extends HTMLElement {
|
||||
// clang-format off
|
||||
LitHtml.render(LitHtml.html`
|
||||
<style>
|
||||
devtools-report {
|
||||
--name-column-width: 150px;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: var(--monospace-font-size);
|
||||
@@ -77,9 +81,23 @@ export class RequestTrustTokensReport extends HTMLElement {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<devtools-report>
|
||||
${this.renderParameterSection()}
|
||||
${this.renderResultSection()}
|
||||
</devtools-report>
|
||||
`, this.shadow);
|
||||
// clang-format on
|
||||
@@ -89,16 +107,14 @@ export class RequestTrustTokensReport extends HTMLElement {
|
||||
if (!this.trustTokenData || !this.trustTokenData.params) {
|
||||
return LitHtml.nothing;
|
||||
}
|
||||
// Disabled until https://crbug.com/1079231 is fixed.
|
||||
// clang-format off
|
||||
|
||||
return LitHtml.html`
|
||||
<devtools-report-section .data=${{sectionTitle: ls`Parameters`} as Components.ReportView.ReportSectionData}>
|
||||
${renderRowWithCodeValue(ls`Type`, this.trustTokenData.params.type.toString())}
|
||||
${this.renderRefreshPolicy(this.trustTokenData.params)}
|
||||
${this.renderIssuers(this.trustTokenData.params)}
|
||||
</devtools-report-section>
|
||||
`;
|
||||
// clang-format on
|
||||
${this.renderIssuerAndTopLevelOriginFromResult()}
|
||||
</devtools-report-section>`;
|
||||
}
|
||||
|
||||
private renderRefreshPolicy(params: Protocol.Network.TrustTokenParams): LitHtml.TemplateResult|{} {
|
||||
@@ -113,30 +129,126 @@ export class RequestTrustTokensReport extends HTMLElement {
|
||||
return LitHtml.nothing;
|
||||
}
|
||||
|
||||
// Disabled until https://crbug.com/1079231 is fixed.
|
||||
// clang-format off
|
||||
return LitHtml.html`
|
||||
<devtools-report-row>
|
||||
<span slot="name">${ls`Issuers`}</span>
|
||||
<ul slot="value" class="issuers-list">
|
||||
${params.issuers.map(issuer => LitHtml.html`<li>${issuer}</li>`)}
|
||||
</ul>
|
||||
</devtools-report-row>
|
||||
`;
|
||||
// clang-format on
|
||||
</devtools-report-row>`;
|
||||
}
|
||||
|
||||
// The issuer and top level origin are technically parameters but reported in the
|
||||
// result structure due to the timing when they are calculated in the backend.
|
||||
// Nonetheless, we show them as part of the parameter section.
|
||||
private renderIssuerAndTopLevelOriginFromResult(): LitHtml.TemplateResult|{} {
|
||||
if (!this.trustTokenData || !this.trustTokenData.result) {
|
||||
return LitHtml.nothing;
|
||||
}
|
||||
|
||||
return LitHtml.html`
|
||||
${renderSimpleRowIfValuePresent(ls`Top level origin`, this.trustTokenData.result.topLevelOrigin)}
|
||||
${renderSimpleRowIfValuePresent(ls`Issuer`, this.trustTokenData.result.issuerOrigin)}`;
|
||||
}
|
||||
|
||||
private renderResultSection(): LitHtml.TemplateResult|{} {
|
||||
if (!this.trustTokenData || !this.trustTokenData.result) {
|
||||
return LitHtml.nothing;
|
||||
}
|
||||
return LitHtml.html`
|
||||
<devtools-report-section .data=${{sectionTitle: ls`Result`} as Components.ReportView.ReportSectionData}>
|
||||
<devtools-report-row>
|
||||
<span slot="name">${ls`Status`}</span>
|
||||
<div slot="value" class="status-row">
|
||||
<devtools-icon class="status-icon"
|
||||
.data=${getIconForStatusCode(this.trustTokenData.result.status)}>
|
||||
</devtools-icon>
|
||||
<div class="status-text">
|
||||
<span><strong>${getSimplifiedStatusTextForStatusCode(this.trustTokenData.result.status)}</strong></span>
|
||||
<span>${getDetailedTextForStatusCode(this.trustTokenData.result.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</devtools-report-row>
|
||||
${this.renderIssuedTokenCount(this.trustTokenData.result)}
|
||||
</devtools-report-section>`;
|
||||
}
|
||||
|
||||
private renderIssuedTokenCount(result: Protocol.Network.TrustTokenOperationDoneEvent): LitHtml.TemplateResult|{} {
|
||||
if (result.type !== Protocol.Network.TrustTokenOperationType.Issuance) {
|
||||
return LitHtml.nothing;
|
||||
}
|
||||
return renderSimpleRowIfValuePresent(ls`Number of issued tokens`, result.issuedTokenCount);
|
||||
}
|
||||
}
|
||||
|
||||
const SUCCESS_ICON_DATA: Components.Icon.IconWithName = {
|
||||
color: 'rgb(12, 164, 12)',
|
||||
iconName: 'ic_checkmark_16x16',
|
||||
width: '12px',
|
||||
};
|
||||
|
||||
const FAILURE_ICON_DATA: Components.Icon.IconWithName = {
|
||||
color: '',
|
||||
iconName: 'error_icon',
|
||||
width: '12px',
|
||||
};
|
||||
|
||||
function statusConsideredSuccess(status: Protocol.Network.TrustTokenOperationDoneEventStatus): boolean {
|
||||
return status === Protocol.Network.TrustTokenOperationDoneEventStatus.Ok ||
|
||||
status === Protocol.Network.TrustTokenOperationDoneEventStatus.AlreadyExists ||
|
||||
status === Protocol.Network.TrustTokenOperationDoneEventStatus.FulfilledLocally;
|
||||
}
|
||||
|
||||
function getIconForStatusCode(status: Protocol.Network.TrustTokenOperationDoneEventStatus):
|
||||
Components.Icon.IconWithName {
|
||||
return statusConsideredSuccess(status) ? SUCCESS_ICON_DATA : FAILURE_ICON_DATA;
|
||||
}
|
||||
|
||||
function getSimplifiedStatusTextForStatusCode(status: Protocol.Network.TrustTokenOperationDoneEventStatus): string {
|
||||
return statusConsideredSuccess(status) ? ls`Success` : ls`Failure`;
|
||||
}
|
||||
|
||||
function getDetailedTextForStatusCode(status: Protocol.Network.TrustTokenOperationDoneEventStatus): string|null {
|
||||
switch (status) {
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.Ok:
|
||||
return null;
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.AlreadyExists:
|
||||
return ls`The operations result was served from cache.`;
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.FulfilledLocally:
|
||||
return ls`The operation was fulfilled locally, no request was sent.`;
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.InvalidArgument:
|
||||
return ls`A client-provided argument was malformed or otherwise invalid.`;
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.ResourceExhausted:
|
||||
return ls`Either no inputs for this operation are available or the output exceeds the operations quota.`;
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.BadResponse:
|
||||
return ls`The servers response was malformed or otherwise invalid.`;
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.FailedPrecondition:
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.Unavailable:
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.InternalError:
|
||||
case Protocol.Network.TrustTokenOperationDoneEventStatus.UnknownError:
|
||||
return ls`The operation failed for an unknown reason.`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSimpleRowIfValuePresent<T>(name: string, value: T|undefined): LitHtml.TemplateResult|{} {
|
||||
if (value === undefined) {
|
||||
return LitHtml.nothing;
|
||||
}
|
||||
|
||||
return LitHtml.html`
|
||||
<devtools-report-row>
|
||||
<span slot="name">${name}</span>
|
||||
<span slot="value">${value}</span>
|
||||
</devtools-report-row>`;
|
||||
}
|
||||
|
||||
function renderRowWithCodeValue(name: string, value: string): LitHtml.TemplateResult {
|
||||
// Disabled until https://crbug.com/1079231 is fixed.
|
||||
// clang-format off
|
||||
return LitHtml.html`
|
||||
<devtools-report-row>
|
||||
<span slot="name">${name}</span>
|
||||
<span slot="value" class="code">${value}</span>
|
||||
</devtools-report-row>
|
||||
`;
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
customElements.define('devtools-trust-token-report', RequestTrustTokensReport);
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<message name="IDS_DEVTOOLS_01a8f976083c550f4d5ba8452ec80efb" desc="Placeholder text content in Network Manage Custom Headers View of the Network panel">
|
||||
No custom headers
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_0216b3e6bdde850491761492bd053587" desc="Text for an error status in the Network panel">
|
||||
Either no inputs for this operation are available or the output exceeds the operations quota.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_02d56cf7754ce16d7ce0193fbca7c90a" desc="Text in Network Config View of the Network panel">
|
||||
Caching
|
||||
</message>
|
||||
@@ -60,6 +63,9 @@
|
||||
<message name="IDS_DEVTOOLS_103b018f0bab1e8f397fd9e932d824a8" desc="Label of a tab in the network panel">
|
||||
Trust Tokens
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_110903d0653aa54aea2780d9b278ca65" desc="Detailed text for a success status in the Network panel">
|
||||
The operations result was served from cache.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_117068a6e55846888cbb60978fe82ca2" desc="Text in Request Timing View of the Network panel">
|
||||
Stalled
|
||||
</message>
|
||||
@@ -204,6 +210,9 @@
|
||||
<message name="IDS_DEVTOOLS_39b8414d89adf80e97ec0024e13e3b86" desc="Title of a section in Request Initiator view of the Network Panel">
|
||||
Request call stack
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_3a8c4b2a369555fa89a8d3eb1ad69a67" desc="Label for a field in the Network panel">
|
||||
Number of issued tokens
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_3b5904efd4efd03d09ee9a8f964f39d6" desc="Text in Network Log View of the Network panel">
|
||||
<ph name="NUMBER_BYTESTOSTRING_RESOURCESIZE_">$1s<ex>4</ex></ph> resources
|
||||
</message>
|
||||
@@ -312,6 +321,9 @@
|
||||
<message name="IDS_DEVTOOLS_5023e5b1d1c02cf168a9a67dda6d2d87" desc="Text in Network Panel of the Network panel">
|
||||
Show overview
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_505a83f220c02df2f85c3810cd9ceb38" desc="Text for the success status in the Network panel">
|
||||
Success
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_50ca42b24dad676e8915c5ebe45e4654" desc="Text in Network Data Grid Node of the Network panel">
|
||||
CoepFrameResourceNeedsCoepHeader
|
||||
</message>
|
||||
@@ -462,12 +474,18 @@
|
||||
<message name="IDS_DEVTOOLS_91e02cd2b8621d0c05197f645668c5c4" desc="A tag of Group Network by frame setting that can be searched in the command menu">
|
||||
network
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_8eea62084ca7e541d918e823422bd82e" desc="Heading of a report section in the Network panel">
|
||||
Result
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_95059da3555281e1df90b77ce4ff7554" desc="Text in Request Timing View of the Network panel">
|
||||
respondWith
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_9b5e92790ca7fec879dfd28355a224d1" desc="Text used to show that serviceworker fetch response source is HTTP cache">
|
||||
From HTTP cache
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_9d58d8f035977c2c392894b3e0c0355b" desc="Detailed text for a success status in the Network panel">
|
||||
The operation was fulfilled locally, no request was sent.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_9dffbf69ffba8bc38bc4e01abf4b1675" desc="Generic label for any text">
|
||||
Text
|
||||
</message>
|
||||
@@ -477,6 +495,9 @@
|
||||
<message name="IDS_DEVTOOLS_85456468f5810641fba6ec7ee35aba75" desc="Tooltip providing the cors error code">
|
||||
Cross-Origin Resource Sharing error: <ph name="CORSERRORSTATUS_CORSERROR">$1s<ex>PreflightDisallowedRedirect</ex></ph>
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_a5ceb7b6d6f16b52bf5a25bb439fc4d1" desc="Text for an error status in the Network panel">
|
||||
The servers response was malformed or otherwise invalid.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_a65043efa3ffbf89834b6d9c362d56a8" desc="Comment used in decoded X-Client-Data HTTP header output in Headers View of the Network panel">
|
||||
Active client experiment variation IDs.
|
||||
</message>
|
||||
@@ -522,6 +543,9 @@
|
||||
<message name="IDS_DEVTOOLS_ca092ce554360617d7bb503ff16dcb7c" desc="Text for toggling the view of header data (e.g. query string parameters) from parsed to source in the headers tab">
|
||||
View source
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_cc9fc0cc521f44c2193f7436c88efa4a" desc="Text for an error status in the Network panel">
|
||||
A client-provided argument was malformed or otherwise invalid.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_ce89cbfed44c10f5dd21427be154f288" desc="Text in Network Log View of the Network panel">
|
||||
Copy as Node.js fetch
|
||||
</message>
|
||||
@@ -570,6 +594,12 @@
|
||||
<message name="IDS_DEVTOOLS_ded747188d830e091a643af558251af5" desc="Tooltip for capture screenshot network setting">
|
||||
Capture screenshots when loading a page
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_e139a585510a502bbf1841cf589f5086" desc="Text in the network panel for an error status">
|
||||
Failure
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_e1f3ad1624ede4a855aafe3c0edbcc34" desc="Label for a report field in the Network panel">
|
||||
Top level origin
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_e2cfb91cd8426f99401143d41343faff" desc="Text in Headers View of the Network panel">
|
||||
To embed this frame in your document, the response needs to enable the cross-origin embedder policy by specifying the following response header:
|
||||
</message>
|
||||
@@ -597,6 +627,9 @@
|
||||
<message name="IDS_DEVTOOLS_eb902cf204f3e4dfffeb56d92a9b5c26" desc="Text to show since when an item is valid">
|
||||
Valid from
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_efb4632d4672f8346140d3fbd42cce29" desc="Text for an error status in the Network panel">
|
||||
The operation failed for an unknown reason.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_f0c5b7ae2d085f65b7b003d43a7d7521" desc="Name of storage cache from which a response was fetched">
|
||||
Cache storage cache name: <ph name="THIS__REQUEST_RESPONSECACHESTORAGECACHENAME">$1s<ex>v1</ex></ph>
|
||||
</message>
|
||||
|
||||
@@ -149,6 +149,7 @@ export class ReportRow extends HTMLElement {
|
||||
.value {
|
||||
color: var(--color-text-primary);
|
||||
flex: auto;
|
||||
margin-inline-start: 0px;
|
||||
padding: 0 6px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import * as Network from '../../../../front_end/network/network.js';
|
||||
|
||||
import {assertShadowRoot, renderElementIntoDOM} from '../helpers/DOMHelpers.js';
|
||||
import {assertNotNull, getElementsWithinComponent, getElementWithinComponent, renderElementIntoDOM} from '../helpers/DOMHelpers.js';
|
||||
|
||||
const {assert} = chai;
|
||||
|
||||
describe('RequestTrustTokensView', () => {
|
||||
it('renders the RefreshPolicy for redemptions', () => {
|
||||
const renderRequestTrustTokensView = () => {
|
||||
const component = new Network.RequestTrustTokensView.RequestTrustTokensReport();
|
||||
renderElementIntoDOM(component);
|
||||
assertShadowRoot(component.shadowRoot);
|
||||
return component;
|
||||
};
|
||||
|
||||
it('renders the RefreshPolicy for redemptions', () => {
|
||||
const component = renderRequestTrustTokensView();
|
||||
component.data = {
|
||||
params: {
|
||||
type: Protocol.Network.TrustTokenOperationType.Redemption,
|
||||
@@ -21,16 +23,13 @@ describe('RequestTrustTokensView', () => {
|
||||
},
|
||||
} as Network.RequestTrustTokensView.RequestTrustTokensReportData;
|
||||
|
||||
const [typeSpan, refreshPolicySpan] = component.shadowRoot.querySelectorAll('span.code');
|
||||
const [typeSpan, refreshPolicySpan] = getElementsWithinComponent(component, 'span.code', HTMLElement);
|
||||
assert.strictEqual(typeSpan.textContent, 'Redemption');
|
||||
assert.strictEqual(refreshPolicySpan.textContent, 'UseCached');
|
||||
});
|
||||
|
||||
it('renders all issuers as a list', () => {
|
||||
const component = new Network.RequestTrustTokensView.RequestTrustTokensReport();
|
||||
renderElementIntoDOM(component);
|
||||
assertShadowRoot(component.shadowRoot);
|
||||
|
||||
const component = renderRequestTrustTokensView();
|
||||
const expectedIssuers = ['example.org', 'foo.dev', 'bar.com'];
|
||||
component.data = {
|
||||
params: {
|
||||
@@ -39,9 +38,39 @@ describe('RequestTrustTokensView', () => {
|
||||
},
|
||||
} as Network.RequestTrustTokensView.RequestTrustTokensReportData;
|
||||
|
||||
const issuerElements = component.shadowRoot.querySelectorAll('ul.issuers-list > li');
|
||||
const issuerElements = getElementsWithinComponent(component, 'ul.issuers-list > li', HTMLElement);
|
||||
const actualIssuers = [...issuerElements].map(e => e.textContent);
|
||||
|
||||
assert.deepStrictEqual(actualIssuers.sort(), expectedIssuers.sort());
|
||||
});
|
||||
|
||||
it('renders a result section with success status for successful requests', () => {
|
||||
const component = renderRequestTrustTokensView();
|
||||
component.data = {
|
||||
result: {
|
||||
status: Protocol.Network.TrustTokenOperationDoneEventStatus.Ok,
|
||||
type: Protocol.Network.TrustTokenOperationType.Issuance,
|
||||
requestId: 'mockId',
|
||||
},
|
||||
};
|
||||
|
||||
const simpleText = getElementWithinComponent(component, 'div.status-text > span', HTMLElement);
|
||||
assertNotNull(simpleText);
|
||||
assert.strictEqual(simpleText.textContent, 'Success');
|
||||
});
|
||||
|
||||
it('renders a result section with failure status for failed requests', () => {
|
||||
const component = renderRequestTrustTokensView();
|
||||
component.data = {
|
||||
result: {
|
||||
status: Protocol.Network.TrustTokenOperationDoneEventStatus.BadResponse,
|
||||
type: Protocol.Network.TrustTokenOperationType.Issuance,
|
||||
requestId: 'mockId',
|
||||
},
|
||||
};
|
||||
|
||||
const simpleText = getElementWithinComponent(component, 'div.status-text > span', HTMLElement);
|
||||
assertNotNull(simpleText);
|
||||
assert.strictEqual(simpleText.textContent, 'Failure');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user