Click to show feature link samples in admin page (#3280)

* Add feature_links_samples API

* Add feature links samples UI

* Fix code style
This commit is contained in:
Ping 2023-08-29 22:21:21 +08:00 коммит произвёл GitHub
Родитель 593968b95d
Коммит 34114d855a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 144 добавлений и 19 удалений

Просмотреть файл

@ -16,7 +16,7 @@
from framework import basehandlers
from internals.core_enums import *
from internals.core_models import FeatureEntry
from internals.feature_links import get_feature_links_summary, get_by_feature_id
from internals.feature_links import get_feature_links_summary, get_by_feature_id, get_feature_links_samples
from framework import permissions
class FeatureLinksAPI(basehandlers.APIHandler):
@ -44,8 +44,19 @@ class FeatureLinksAPI(basehandlers.APIHandler):
class FeatureLinksSummaryAPI(basehandlers.APIHandler):
"""FeatureLinksSummaryAPI will return all links to the client."""
"""FeatureLinksSummaryAPI will return summary of links to the client."""
@permissions.require_admin_site
def do_get(self, **kwargs):
return get_feature_links_summary()
return get_feature_links_summary()
class FeatureLinksSamplesAPI(basehandlers.APIHandler):
"""FeatureLinksSamplesAPI will return sample links to the client."""
@permissions.require_admin_site
def do_get(self, **kwargs):
domain = self.request.args.get('domain', None)
type = self.request.args.get('type', None)
is_error = self.get_bool_arg('is_error', None)
if domain:
return get_feature_links_samples(domain, type, is_error)

Просмотреть файл

@ -1,4 +1,4 @@
import {LitElement, css, html} from 'lit';
import {LitElement, css, html, nothing} from 'lit';
import {showToastMessage} from './utils.js';
import {SHARED_STYLES} from '../css/shared-css.js';
import {VARS} from '../css/_vars-css.js';
@ -18,18 +18,29 @@ export class ChromedashAdminFeatureLinksPage extends LitElement {
justify-content: space-between;
font-size: 16px;
}
.feature-links-samples .line {
background: rgb(232,234,237);
}
sl-icon-button::part(base) {
padding: 0;
margin-left: 8px;
}
`];
}
static get properties() {
return {
loading: {type: Boolean},
featureLinkSummary: {type: Object},
sampleId: {type: String},
samplesLoading: {type: Boolean},
featureLinksSamples: {type: Array},
featureLinksSummary: {type: Object},
};
}
constructor() {
super();
this.featureLinkSummary = [];
this.featureLinksSummary = {};
this.featureLinksSamples = [];
}
connectedCallback() {
@ -40,7 +51,7 @@ export class ChromedashAdminFeatureLinksPage extends LitElement {
async fetchData() {
try {
this.loading = true;
this.featureLinkSummary = await window.csClient.getFeatureLinkSummary();
this.featureLinksSummary = await window.csClient.getFeatureLinksSummary();
} catch {
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
} finally {
@ -48,34 +59,92 @@ export class ChromedashAdminFeatureLinksPage extends LitElement {
}
}
calcSampleId(domain, type, isError) {
return `domain=${domain}&type=${type}&isError=${isError}`;
}
async fetchLinkSamples(domain, type, isError) {
this.sampleId = this.calcSampleId(domain, type, isError);
this.featureLinksSamples = [];
this.samplesLoading = true;
try {
this.featureLinksSamples = await
window.csClient.getFeatureLinksSamples(domain, type, isError);
} catch {
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
} finally {
this.samplesLoading = false;
}
}
renderSamples() {
if (this.samplesLoading) {
return html`<sl-spinner></sl-spinner>`;
}
return html`
<div class="feature-links-samples">
${this.featureLinksSamples.map((sample) => html`
<div class="line">
<div>
<a href=${sample.url}><i>${sample.url}</i></a>
${sample.http_error_code ? html`<i>(${sample.http_error_code})</i>` : nothing}
</div>
<a href=${`/feature/${sample.feature_ids}`} target="_blank" rel="noopener">
<sl-icon library="material" name="link" slot="prefix" title="linked feature"></sl-icon>
</a>
</div>
`)}
</div>
`;
}
renderComponents() {
return html`
<div class="feature-links-summary">
<sl-details summary="Link Summary" open>
<div class="line">All Links <b>${this.featureLinkSummary.total_count}</b></div>
<div class="line">Covered Links <b>${this.featureLinkSummary.covered_count}</b></div>
<div class="line">Uncovered (aka "web") Links <b>${this.featureLinkSummary.uncovered_count}</b></div>
<div class="line">All Error Links<b>${this.featureLinkSummary.error_count}</b></div>
<div class="line">HTTP Error Links<b>${this.featureLinkSummary.http_error_count}</b></div>
<div class="line">All Links <b>${this.featureLinksSummary.total_count}</b></div>
<div class="line">Covered Links <b>${this.featureLinksSummary.covered_count}</b></div>
<div class="line">Uncovered (aka "web") Links <b>${this.featureLinksSummary.uncovered_count}</b></div>
<div class="line">All Error Links<b>${this.featureLinksSummary.error_count}</b></div>
<div class="line">HTTP Error Links<b>${this.featureLinksSummary.http_error_count}</b></div>
</sl-details>
<sl-details summary="Link Types" open>
${this.featureLinkSummary.link_types.map((linkType) => html`
${this.featureLinksSummary.link_types.map((linkType) => html`
<div class="line">${(linkType.key).toUpperCase()} <b>${linkType.count}</b></div>
`)}
</sl-details>
<sl-details summary="Uncovered Link Domains" open>
${this.featureLinkSummary.uncovered_link_domains.map((domain) => html`
<div class="line"><a href=${domain.key}>${domain.key}</a> <b>${domain.count}</b></div>
${this.featureLinksSummary.uncovered_link_domains.map((domain) => html`
<div class="line">
<div>
<a href=${domain.key}>${domain.key}</a>
<sl-icon-button library="material" name="search" slot="prefix" title="Samples"
@click=${() => this.fetchLinkSamples(domain.key, 'web', undefined)}>
></sl-icon-button>
</div>
<b>${domain.count}</b>
</div>
${this.sampleId === this.calcSampleId(domain.key, 'web', undefined) ? this.renderSamples() : nothing}
`)}
</sl-details>
<sl-details summary="Error Link Domains" open>
${this.featureLinkSummary.error_link_domains.map((domain) => html`
<div class="line"><a href=${domain.key}>${domain.key}</a> <b>${domain.count}</b></div>
`)}
${this.featureLinksSummary.error_link_domains.map((domain) => html`
<div class="line">
<div>
<a href=${domain.key}>${domain.key}</a>
<sl-icon-button library="material" name="search" slot="prefix" title="Samples"
@click=${() => this.fetchLinkSamples(domain.key, undefined, true)}>
></sl-icon-button>
</div>
<b>${domain.count}</b>
</div>
${this.sampleId === this.calcSampleId(domain.key, undefined, true) ? this.renderSamples() : nothing}
`)}
</sl-details>
</div>
`;
}
render() {
return html`
${this.loading ?

Просмотреть файл

@ -312,10 +312,21 @@ class ChromeStatusClient {
return this.doGet(`/feature_links?feature_id=${featureId}&update_stale_links=${updateStaleLinks}`);
}
getFeatureLinkSummary() {
getFeatureLinksSummary() {
return this.doGet('/feature_links_summary');
}
getFeatureLinksSamples(domain, type, isError) {
let optionalParams = '';
if (type) {
optionalParams += `&type=${type}`;
}
if (isError !== undefined && isError !== null) {
optionalParams += `&is_error=${isError}`;
}
return this.doGet(`/feature_links_samples?domain=${domain}${optionalParams}`);
}
// Stages API
getStage(featureId, stageId) {
return this.doGet(`/features/${featureId}/stages/${stageId}`);

Просмотреть файл

@ -288,6 +288,39 @@ def get_feature_links_summary():
}
def get_feature_links_samples(domain: str, type: str | None, is_error: bool | None):
"""retrieves a list of feature links based on the specified domain, type, and error status."""
MAX_SAMPLES = 100
filters = [
FeatureLinks.url >= domain,
]
if type:
filters.append(FeatureLinks.type == type)
if is_error:
filters.append(FeatureLinks.is_error == is_error)
feature_links = FeatureLinks.query(
*filters
).fetch(MAX_SAMPLES)
# filter out links that do not start with the specified domain and convert to dict
feature_links = [
fl.to_dict(include=['url', 'type', 'feature_ids', 'is_error', 'http_error_code'])
for fl in feature_links if fl.url.startswith(domain)
]
# flatten feature links like projection in get_feature_links_summary
flattened_feature_links = []
for feature_link in feature_links:
for feature_id in feature_link['feature_ids']:
flattened_feature_links.append({
**feature_link,
'feature_ids': feature_id
})
return flattened_feature_links
class UpdateAllFeatureLinksHandlers(FlaskHandler):
def get_template_data(self, **kwargs) -> str:

Просмотреть файл

@ -107,6 +107,7 @@ api_routes: list[Route] = [
Route(f'{API_BASE}/features/create', features_api.FeaturesAPI),
Route(f'{API_BASE}/feature_links', feature_links_api.FeatureLinksAPI),
Route(f'{API_BASE}/feature_links_summary', feature_links_api.FeatureLinksSummaryAPI),
Route(f'{API_BASE}/feature_links_samples', feature_links_api.FeatureLinksSamplesAPI),
Route(f'{API_BASE}/features/<int:feature_id>/votes',
reviews_api.VotesAPI),
Route(f'{API_BASE}/features/<int:feature_id>/votes/<int:gate_id>',