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:
Родитель
593968b95d
Коммит
34114d855a
|
@ -16,7 +16,7 @@
|
||||||
from framework import basehandlers
|
from framework import basehandlers
|
||||||
from internals.core_enums import *
|
from internals.core_enums import *
|
||||||
from internals.core_models import FeatureEntry
|
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
|
from framework import permissions
|
||||||
|
|
||||||
class FeatureLinksAPI(basehandlers.APIHandler):
|
class FeatureLinksAPI(basehandlers.APIHandler):
|
||||||
|
@ -44,8 +44,19 @@ class FeatureLinksAPI(basehandlers.APIHandler):
|
||||||
|
|
||||||
|
|
||||||
class FeatureLinksSummaryAPI(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
|
@permissions.require_admin_site
|
||||||
def do_get(self, **kwargs):
|
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 {showToastMessage} from './utils.js';
|
||||||
import {SHARED_STYLES} from '../css/shared-css.js';
|
import {SHARED_STYLES} from '../css/shared-css.js';
|
||||||
import {VARS} from '../css/_vars-css.js';
|
import {VARS} from '../css/_vars-css.js';
|
||||||
|
@ -18,18 +18,29 @@ export class ChromedashAdminFeatureLinksPage extends LitElement {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 16px;
|
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() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
loading: {type: Boolean},
|
loading: {type: Boolean},
|
||||||
featureLinkSummary: {type: Object},
|
sampleId: {type: String},
|
||||||
|
samplesLoading: {type: Boolean},
|
||||||
|
featureLinksSamples: {type: Array},
|
||||||
|
featureLinksSummary: {type: Object},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.featureLinkSummary = [];
|
this.featureLinksSummary = {};
|
||||||
|
this.featureLinksSamples = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
@ -40,7 +51,7 @@ export class ChromedashAdminFeatureLinksPage extends LitElement {
|
||||||
async fetchData() {
|
async fetchData() {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.featureLinkSummary = await window.csClient.getFeatureLinkSummary();
|
this.featureLinksSummary = await window.csClient.getFeatureLinksSummary();
|
||||||
} catch {
|
} catch {
|
||||||
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
|
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
|
||||||
} finally {
|
} 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() {
|
renderComponents() {
|
||||||
return html`
|
return html`
|
||||||
<div class="feature-links-summary">
|
<div class="feature-links-summary">
|
||||||
<sl-details summary="Link Summary" open>
|
<sl-details summary="Link Summary" open>
|
||||||
<div class="line">All Links <b>${this.featureLinkSummary.total_count}</b></div>
|
<div class="line">All Links <b>${this.featureLinksSummary.total_count}</b></div>
|
||||||
<div class="line">Covered Links <b>${this.featureLinkSummary.covered_count}</b></div>
|
<div class="line">Covered Links <b>${this.featureLinksSummary.covered_count}</b></div>
|
||||||
<div class="line">Uncovered (aka "web") Links <b>${this.featureLinkSummary.uncovered_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.featureLinkSummary.error_count}</b></div>
|
<div class="line">All Error Links<b>${this.featureLinksSummary.error_count}</b></div>
|
||||||
<div class="line">HTTP Error Links<b>${this.featureLinkSummary.http_error_count}</b></div>
|
<div class="line">HTTP Error Links<b>${this.featureLinksSummary.http_error_count}</b></div>
|
||||||
</sl-details>
|
</sl-details>
|
||||||
<sl-details summary="Link Types" open>
|
<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>
|
<div class="line">${(linkType.key).toUpperCase()} <b>${linkType.count}</b></div>
|
||||||
`)}
|
`)}
|
||||||
</sl-details>
|
</sl-details>
|
||||||
<sl-details summary="Uncovered Link Domains" open>
|
<sl-details summary="Uncovered Link Domains" open>
|
||||||
${this.featureLinkSummary.uncovered_link_domains.map((domain) => html`
|
${this.featureLinksSummary.uncovered_link_domains.map((domain) => html`
|
||||||
<div class="line"><a href=${domain.key}>${domain.key}</a> <b>${domain.count}</b></div>
|
<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>
|
||||||
<sl-details summary="Error Link Domains" open>
|
<sl-details summary="Error Link Domains" open>
|
||||||
${this.featureLinkSummary.error_link_domains.map((domain) => html`
|
${this.featureLinksSummary.error_link_domains.map((domain) => html`
|
||||||
<div class="line"><a href=${domain.key}>${domain.key}</a> <b>${domain.count}</b></div>
|
<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>
|
</sl-details>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
${this.loading ?
|
${this.loading ?
|
||||||
|
|
|
@ -312,10 +312,21 @@ class ChromeStatusClient {
|
||||||
return this.doGet(`/feature_links?feature_id=${featureId}&update_stale_links=${updateStaleLinks}`);
|
return this.doGet(`/feature_links?feature_id=${featureId}&update_stale_links=${updateStaleLinks}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatureLinkSummary() {
|
getFeatureLinksSummary() {
|
||||||
return this.doGet('/feature_links_summary');
|
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
|
// Stages API
|
||||||
getStage(featureId, stageId) {
|
getStage(featureId, stageId) {
|
||||||
return this.doGet(`/features/${featureId}/stages/${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):
|
class UpdateAllFeatureLinksHandlers(FlaskHandler):
|
||||||
|
|
||||||
def get_template_data(self, **kwargs) -> str:
|
def get_template_data(self, **kwargs) -> str:
|
||||||
|
|
1
main.py
1
main.py
|
@ -107,6 +107,7 @@ api_routes: list[Route] = [
|
||||||
Route(f'{API_BASE}/features/create', features_api.FeaturesAPI),
|
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', feature_links_api.FeatureLinksAPI),
|
||||||
Route(f'{API_BASE}/feature_links_summary', feature_links_api.FeatureLinksSummaryAPI),
|
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',
|
Route(f'{API_BASE}/features/<int:feature_id>/votes',
|
||||||
reviews_api.VotesAPI),
|
reviews_api.VotesAPI),
|
||||||
Route(f'{API_BASE}/features/<int:feature_id>/votes/<int:gate_id>',
|
Route(f'{API_BASE}/features/<int:feature_id>/votes/<int:gate_id>',
|
||||||
|
|
Загрузка…
Ссылка в новой задаче