Remove WIP files
This commit is contained in:
Родитель
3fd617fe66
Коммит
f1c50d6506
|
@ -1,132 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
import '/static/elements/chromedash-color-status.js';
|
||||
|
||||
class ChromedashLegend extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
opened: {type: Boolean, reflect: true},
|
||||
views: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.opened = false;
|
||||
this.views = [];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this._dissmissEl = this.$.querySelector('[dialog-dismiss]');
|
||||
if (this._dissmissEl) {
|
||||
this.listen(this._dissmissEl, 'click', 'toggle');
|
||||
}
|
||||
}
|
||||
|
||||
detached() {
|
||||
if (this.this._dissmissEl) {
|
||||
this.unlisten(this._dissmissEl, 'click', 'toggle');
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.opened = !this.opened;
|
||||
document.body.style.overflow = this.opened ? 'hidden' : '';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/css/elements/chromedash-legend.css">
|
||||
|
||||
<div id="overlay">
|
||||
<h3>About the data</h3>
|
||||
<section class="content-wrapper">
|
||||
<p class="description">What you're looking at is a mostly
|
||||
comprehensive list of web platform features that have landed in
|
||||
Chromium, ordered chronologically by the milestone in which they were
|
||||
added. Features marked "No active development" are being considered or
|
||||
have yet to be started. Features marked "In development" are currently
|
||||
being worked on.</p>
|
||||
<div class="close buttons">
|
||||
<iron-icon icon="chromestatus:close" dialog-dismiss></iron-icon>
|
||||
</div>
|
||||
</section>
|
||||
<h3>Color legend</h3>
|
||||
<p>Colors indicate the "interoperability risk" for a given feature. The
|
||||
risk increases as
|
||||
<chromedash-color-status .value="1"
|
||||
.max="${views.vendors.length}"></chromedash-color-status> →
|
||||
<chromedash-color-status .value="${views.vendors.length}"
|
||||
.max="${views.vendors.length}"></chromedash-color-status>, and the
|
||||
color meaning differs for browser vendors, web developers, and the
|
||||
standards process.</p>
|
||||
<section class="views">
|
||||
<div>
|
||||
<label>Browser vendors</label>
|
||||
<ul>
|
||||
${this.views.vendors.map((vendor) => html`
|
||||
<li>
|
||||
<chromedash-color-status .value="${vendor.key}"
|
||||
.max="${views.vendors.length}">
|
||||
</chromedash-color-status>
|
||||
<span>${vendor.val}</span></li>
|
||||
`)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<label>Web developer</label>
|
||||
<ul>
|
||||
${this.views.webdevs.map((webdev) => html`
|
||||
<li>
|
||||
<chromedash-color-status .value="${webdev.key}"
|
||||
.max="${views.webdevs.length}">
|
||||
</chromedash-color-status>
|
||||
<span>${webdev.val}</span></li>
|
||||
`)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<label>Standards values</label>
|
||||
<ul>
|
||||
${this.views.standards.map((standard) => html`
|
||||
<li>
|
||||
<chromedash-color-status .value="${standard.key}"
|
||||
.max="${views.standards.length}">
|
||||
</chromedash-color-status>
|
||||
<span>${standard.val}</span></li>
|
||||
`)}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<h3>Search</h3>
|
||||
<section>
|
||||
<label>Example search queries</label>
|
||||
<ul class="queries">
|
||||
<li>
|
||||
<span>"browsers.chrome.desktop<30"</span>features that landed before 30
|
||||
</li>
|
||||
<li>
|
||||
<span>"browsers.chrome.desktop<=50"</span>features in desktop chrome 50
|
||||
</li>
|
||||
<li>
|
||||
<span>"browsers.chrome.android>40"</span>features that landed on Chrome Android after 40 (milestone types: browsers.chrome.android, browsers.chrome.ios, browsers.chrome.webview)
|
||||
</li>
|
||||
<li>
|
||||
<span>"browsers.chrome.status.val=4"</span>features behind a flag
|
||||
</li>
|
||||
<li>
|
||||
<span>"category:CSS"</span>features in the CSS category
|
||||
</li>
|
||||
<li>
|
||||
<span>"component:Blink>CSS"</span>features under the Blink component "Blink>CSS"
|
||||
</li>
|
||||
<li>
|
||||
<span>"browsers.chrome.owners:user@example.org"</span>features owned by user@example.org
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-color-status', ChromedashLegend);
|
|
@ -1,153 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
import 'https://unpkg.com/@polymer/iron-icon/iron-icon.js?module';
|
||||
import '/static/elements/chromedash-x-meter.js';
|
||||
|
||||
class ChromedashMetrics extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
type: {type: String}, // From attribute
|
||||
view: {type: String}, // From attribute
|
||||
useRemoteData: {type: Boolean}, // From attibute. If true, fetches live data from chromestatus.com instead of localhost.
|
||||
// Properties used in the template
|
||||
viewList: {type: Array},
|
||||
propertyNameSortIcon: {type: String},
|
||||
percentSortIcon: {type: String},
|
||||
maxPercentage: {type: Number},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.viewList = [];
|
||||
this.type = '';
|
||||
this.useRemoteData = false;
|
||||
this.maxPercentage = 100;
|
||||
this.sortOrders = {
|
||||
property_name: {reverse: false, activated: false},
|
||||
percentage: {reverse: true, activated: true},
|
||||
};
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const endpoint = `${this.useRemoteData ? 'https://www.chromestatus.com' : ''}/data/${this.type}${this.view}`;
|
||||
|
||||
fetch(endpoint).then((res) => res.json()).then((response) => {
|
||||
this._updateAfterData(response);
|
||||
});
|
||||
}
|
||||
|
||||
_updateAfterData(items) {
|
||||
this.fire('app-ready');
|
||||
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0, item; item = items[i]; ++i) {
|
||||
item.percentage = (item.day_percentage * 100).toFixed(6);
|
||||
}
|
||||
|
||||
this.viewList = items.filter((item) => {
|
||||
return !['ERROR', 'PageVisits', 'PageDestruction'].includes(item.property_name);
|
||||
});
|
||||
|
||||
this.maxPercentage = this.viewList.reduce((accum, currVal) => {
|
||||
return Math.max(accum, currVal.percentage);
|
||||
}, 0);
|
||||
|
||||
if (location.hash) {
|
||||
const hash = decodeURIComponent(location.hash);
|
||||
if (hash) {
|
||||
const el = this.$['stack-rank-list'].querySelector(hash);
|
||||
el.scrollIntoView(true, {behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const order = e.target.dataset.order;
|
||||
sortBy_(order, this.viewList);
|
||||
switch (order) {
|
||||
case 'percentage':
|
||||
this.sortOrders.percentage.activated = true;
|
||||
this.sortOrders.percentage.reverse = !this.sortOrders.percentage.reverse;
|
||||
break;
|
||||
case 'property_name':
|
||||
this.sortOrders.property_name.activated = true;
|
||||
this.sortOrders.property_name.reverse = !this.sortOrders.property_name.reverse;
|
||||
break;
|
||||
}
|
||||
|
||||
this._updateSortIcons();
|
||||
if (this.sortOrders[order].reverse) {
|
||||
this.viewList.reverse();
|
||||
}
|
||||
|
||||
// TODO: remove when github.com/Polymer/polymer/issues/2175 is fixed.
|
||||
this.viewList = this.viewList.slice(0);
|
||||
}
|
||||
|
||||
showTimeline(e) {
|
||||
window.location.href = `/metrics/${this.type}/timeline/${this.view}/${e.model.prop.bucket_id}`;
|
||||
}
|
||||
|
||||
_updateSortIcons() {
|
||||
this.propertyNameSortIcon = this._getOrderIcon('property_name');
|
||||
this.percentSortIcon = this._getOrderIcon('percentage');
|
||||
}
|
||||
|
||||
_getOrderIcon(key) {
|
||||
if (!this.sortOrders[key].activated) {
|
||||
return '';
|
||||
}
|
||||
return this.sortOrders[key].reverse ?
|
||||
'chromestatus:arrow-drop-up' : 'chromestatus:arrow-drop-down';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/css/elements/chromedash-metrics.css">
|
||||
|
||||
<b>Showing <span>${this.viewList.length}</span> properties</b>
|
||||
<ol id="stack-rank-list">
|
||||
<li class="header">
|
||||
<label @click="${this.sort}" data-order="property_name">
|
||||
Property name <iron-icon icon="${this.propertyNameSortIcon}"></iron-icon>
|
||||
</label>
|
||||
<label @click="${this.sort}" data-order="percentage" class="percent_label">
|
||||
Percentage <iron-icon icon="${this.percentSortIcon}"></iron-icon>
|
||||
</label>
|
||||
</li>
|
||||
${this.viewList.map((item) => html`
|
||||
<li id="${item.property_name}"
|
||||
title="${item.property_name}. Click to deep link to this property." tabindex="0">
|
||||
<label>
|
||||
<a href="#${item.property_name}">${item.property_name}</a>
|
||||
</label>
|
||||
<chromedash-x-meter value="${item.percentage}" max="${this.maxPercentage}" @click="${this.showTimeline}"
|
||||
title="Click to see a timeline view of this property"></chromedash-x-meter>
|
||||
</li>
|
||||
`)}
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-metrics', ChromedashMetrics);
|
||||
|
||||
const sortBy_ = (propName, arr) => {
|
||||
const compareAsNumbers = propName === 'percentage' || false;
|
||||
arr.sort((a, b) => {
|
||||
const propA = compareAsNumbers ? Number(a[propName]) : a[propName];
|
||||
const propB = compareAsNumbers ? Number(b[propName]) : b[propName];
|
||||
if (propA > propB) {
|
||||
return 1;
|
||||
}
|
||||
if (propA < propB) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
|
@ -1,170 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
import 'https://unpkg.com/@polymer/iron-icon/iron-icon.js?module';
|
||||
|
||||
class ChromedashSamplePanel extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
categories: {type: Object}, // Edited in static/js/samples.js
|
||||
features: {type: Array}, // Edited in static/js/samples.js
|
||||
filtered: {type: Array}, // Edited in static/js/samples.js
|
||||
prevSelectedCategory: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
_fireEvent(eventName, detail) {
|
||||
let event = new CustomEvent(eventName, {detail});
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
_computeIcon(el) {
|
||||
return 'chromestatus:' + (el ? el.dataset.category : '');
|
||||
}
|
||||
|
||||
_computeIconId(categoryStr) {
|
||||
return 'chromestatus:' + this.categories[categoryStr];
|
||||
}
|
||||
|
||||
_computeFeatureLink(id) {
|
||||
return '/features/' + id;
|
||||
}
|
||||
|
||||
_onSelectCategory() {
|
||||
// Enable toggling selected item until
|
||||
// https://github.com/PolymerElements/paper-menu/issues/50 is fixed.
|
||||
if (this.selectedCategory === this.prevSelectedCategory) {
|
||||
categoryMenuEl.selected = null;
|
||||
this.selectedCategory = null;
|
||||
this.prevSelectedCategory = null;
|
||||
} else {
|
||||
this.prevSelectedCategory = this.selectedCategory;
|
||||
}
|
||||
this.filter(searchEl.value);
|
||||
}
|
||||
|
||||
_filterOnOperation(features, operator, version) {
|
||||
return features.filter((feature) => {
|
||||
const platformMilestones = [
|
||||
parseInt(feature.shipped_milestone),
|
||||
parseInt(feature.shipped_android_milestone),
|
||||
parseInt(feature.shipped_ios_milestone),
|
||||
parseInt(feature.shipped_webview_milestone),
|
||||
];
|
||||
for (let i = 0, milestone; milestone = platformMilestones[i]; ++i) {
|
||||
if (matchesMilestone_(milestone, operator, version)) {
|
||||
return true; // Only one of the platforms needs to match.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: move filtering into a behavior. Mostly duped from chromedash-featurelist.html.
|
||||
filter(val) {
|
||||
let features = this.features;
|
||||
|
||||
if (!features) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!val && !this.selectedCategory) {
|
||||
if (history && history.replaceState) {
|
||||
history.replaceState('', document.title, location.pathname + location.search);
|
||||
} else {
|
||||
location.hash = '';
|
||||
}
|
||||
this.filtered = features;
|
||||
this._fireEvent('update-length', {lenght: this.filtered.length});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (val) {
|
||||
val = val.trim();
|
||||
|
||||
if (history && history.replaceState) {
|
||||
// TODO debounce this 500ms
|
||||
history.replaceState({}, document.title, '/samples#' + val);
|
||||
}
|
||||
|
||||
// Returns operator and version query e.g. ["<=25", "<=", "25"].
|
||||
const operatorMatch = /^([<>=]=?)\s*([0-9]+)/.exec(val);
|
||||
if (operatorMatch) {
|
||||
features = this._filterOnOperation(features, operatorMatch[1], operatorMatch[2]);
|
||||
} else {
|
||||
const regex = new RegExp(val, 'i');
|
||||
features = features.filter((feature) => {
|
||||
return regex.test(feature.name) || regex.test(feature.summary);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Further refine list based on selected category in menu.
|
||||
if (this.selectedCategory) {
|
||||
const category = this.selectedCategory.textContent.trim();
|
||||
const regex = new RegExp(category, 'i');
|
||||
features = features.filter((feature) => {
|
||||
return regex.test(feature.category);
|
||||
});
|
||||
}
|
||||
|
||||
this.filtered = features;
|
||||
|
||||
this._fireEvent('update-length', {lenght: this.filtered.length});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="sample-panel">
|
||||
<ul id="samplelist" class="card-content">
|
||||
${this.filtered.map((feature) => html`
|
||||
<li>
|
||||
<div class="card">
|
||||
<h3 class="feature-name">
|
||||
<a href="${this._computeFeatureLink(feature.id)}">${feature.name}</a>
|
||||
<span class="milestone" ?hidden="${!feature.shipped_milestone}">Chrome <span>${feature.shipped_milestone}</span></span>
|
||||
</h3>
|
||||
<div class="summary">${feature.summary}</div>
|
||||
<div class="sample_links">
|
||||
<div class="demo-links layout horizontal center">
|
||||
${this.feature.sample_links.map((link) => html`
|
||||
<a href="${link}" target="_blank" class="demo-link">
|
||||
<iron-icon icon="chromestatus:open-in-browser"></iron-icon> View Demo
|
||||
</a>
|
||||
`)}
|
||||
</div>
|
||||
<iron-icon icon="${this._computeIconId(feature.category)}"
|
||||
title="${feature.category}"></iron-icon>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-sample-panel', ChromedashSamplePanel);
|
||||
|
||||
// TODO: move this into a behavior. It's duplicated from chromedash-featurelist.html.
|
||||
function matchesMilestone_(milestone, operator, version) {
|
||||
switch (operator) {
|
||||
case '<':
|
||||
return milestone < version;
|
||||
break;
|
||||
case '<=':
|
||||
return milestone <= version;
|
||||
break;
|
||||
case '>':
|
||||
return milestone > version;
|
||||
break;
|
||||
case '>=':
|
||||
return milestone >= version;
|
||||
break;
|
||||
case '=': // Support both '=' and '=='.
|
||||
case '==':
|
||||
return milestone == version;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,257 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
import 'https://unpkg.com/@polymer/iron-icon/iron-icon.js?module';
|
||||
|
||||
class ChromedashSchedule extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
_objKeys(obj) {
|
||||
if (!obj) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(obj).sort();
|
||||
}
|
||||
|
||||
_featuresFor(features, componentName) {
|
||||
return features[componentName];
|
||||
}
|
||||
|
||||
isRemoved(implStatusChrome) {
|
||||
return implStatusChrome === 'Removed';
|
||||
}
|
||||
|
||||
isDeprecated(implStatusChrome) {
|
||||
return implStatusChrome === 'Deprecated' || implStatusChrome === 'No longer pursuing';
|
||||
}
|
||||
|
||||
_computeDaysUntil(dateStr) {
|
||||
const today = new Date();
|
||||
const diff = dateDiffInDays(new Date(dateStr), today);
|
||||
const prefix = diff.future ? 'in' : '';
|
||||
const days = diff.days > 1 ? 's' : '';
|
||||
const ago = !diff.future ? 'ago' : '';
|
||||
return `${prefix} ${diff.days} day${days} ${ago}`;
|
||||
}
|
||||
|
||||
_computeDate(dateStr) {
|
||||
const opts = {/* weekday: 'short', */month: 'short', day: 'numeric'};
|
||||
const date = new Date(dateStr);
|
||||
return new Intl.DateTimeFormat('en-US', opts).format(date);
|
||||
}
|
||||
|
||||
pushDisabled() {
|
||||
return PushNotifier.GRANTED_ACCESS ? '' : 'disabled';
|
||||
}
|
||||
|
||||
_computePushSubscribed(subscribed) {
|
||||
return subscribed ? 'chromestatus:notifications' : 'chromestatus:notifications-off';
|
||||
}
|
||||
|
||||
subscribeToFeature(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const featureId = Polymer.dom(e).event.model.f.id;
|
||||
const icon = Polymer.dom(e).localTarget;
|
||||
const receivePush = icon.icon !== 'chromestatus:notifications';
|
||||
icon.icon = this._computePushSubscribed(receivePush);
|
||||
|
||||
if (receivePush) {
|
||||
PushNotifications.subscribeToFeature(featureId);
|
||||
} else {
|
||||
PushNotifications.unsubscribeFromFeature(featureId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<div class="releases layout horizontal wrap">
|
||||
<section class="release">
|
||||
<div class="layout vertical center">
|
||||
<h1 class="channel_label">stable</h1>
|
||||
<h1 class="chrome_version layout horizontal center">
|
||||
<span class="chrome-logo"></span>
|
||||
<a href="https://www.google.com/chrome/browser/desktop/index.html" target="_blank" title="Download Chrome stable">Chrome [[channels.stable.version]]</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="milestone_info layout horizontal center-center">
|
||||
<h3><span class="channel_label">Beta</span> was <span class="milestone_info-beta">[[_computeDate(channels.stable.earliest_beta)]] - [[_computeDate(channels.stable.latest_beta)]]</span></h3>
|
||||
</div>
|
||||
<div class="milestone_info layout horizontal center-center">
|
||||
<h3><span class="channel_label">Stable</span> [[_computeDaysUntil(channels.stable.stable_date)]]
|
||||
<span class="release-stable">( [[_computeDate(channels.stable.stable_date)]] )</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="features_list">
|
||||
<div class="features_header">Features in this release:</div>
|
||||
|
||||
<template is="dom-repeat" items="[[_objKeys(channels.stable.components)]]" as="componentName">
|
||||
<h3 class="feature_components">{{componentName}}</h3>
|
||||
<ul>
|
||||
<template is="dom-repeat" items="[[_featuresFor(channels.stable.components, componentName)]]" as="f">
|
||||
<li data-feature-id$="[[f.id]]">
|
||||
<a href$="/feature/[[f.id]]">[[f.name]]</a>
|
||||
<span class="icon_row">
|
||||
<template is="dom-if" if="[[f.browsers.chrome.origintrial]]">
|
||||
<span class="tooltip" title="Origin Trial">
|
||||
<iron-icon icon="chromestatus:extension" class="experimental" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[f.browsers.chrome.intervention]]">
|
||||
<span class="tooltip" title="Browser intervention">
|
||||
<iron-icon icon="chromestatus:pan-tool" class="intervention" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isRemoved(f.browsers.chrome.status.text)]]">
|
||||
<span class="tooltip" title="Removed">
|
||||
<iron-icon icon="chromestatus:cancel" class="remove" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isDeprecated(f.browsers.chrome.status.text)]]">
|
||||
<span class="tooltip" title="Deprecated">
|
||||
<iron-icon icon="chromestatus:warning" class="deprecated" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span class="tooltip no-push-notifications" title="Subscribe to notification updates">
|
||||
<iron-icon icon="chromestatus:notifications-off"
|
||||
on-click="subscribeToFeature" class$="pushicon [[pushDisabled()]]"></iron-icon>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section class="release release-beta">
|
||||
<div class="layout vertical center">
|
||||
<h1 class="channel_label">Next up</h1>
|
||||
<h1 class="chrome_version chrome_version--beta layout horizontal center">
|
||||
<span class="chrome-logo"></span>
|
||||
<a href="https://www.google.com/chrome/browser/beta.html" target="_blank" title="Download Chrome Beta">Chrome [[channels.beta.version]]</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="milestone_info layout horizontal center-center">
|
||||
<h3><span class="channel_label">Beta</span> between <span class="milestone_info-beta">[[_computeDate(channels.beta.earliest_beta)]] - [[_computeDate(channels.beta.latest_beta)]]</span></h3>
|
||||
</div>
|
||||
<div class="milestone_info layout horizontal center-center">
|
||||
<h3><span class="channel_label">Stable</span> [[_computeDaysUntil(channels.beta.stable_date)]]
|
||||
<span class="release-stable">( [[_computeDate(channels.beta.stable_date)]] )</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="features_list">
|
||||
<div class="features_header">Features planned in this release:</div>
|
||||
|
||||
<template is="dom-repeat" items="[[_objKeys(channels.beta.components)]]" as="componentName">
|
||||
<h3 class="feature_components">{{componentName}}</h3>
|
||||
<ul>
|
||||
<template is="dom-repeat" items="[[_featuresFor(channels.beta.components, componentName)]]" as="f">
|
||||
<li data-feature-id$="[[f.id]]">
|
||||
<a href$="/feature/[[f.id]]">[[f.name]]</a>
|
||||
<span class="icon_row">
|
||||
<template is="dom-if" if="[[f.browsers.chrome.origintrial]]">
|
||||
<span class="tooltip" title="Origin Trial">
|
||||
<iron-icon icon="chromestatus:extension" class="experimental" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[f.browsers.chrome.intervention]]">
|
||||
<span class="tooltip" title="Browser intervention">
|
||||
<iron-icon icon="chromestatus:pan-tool" class="intervention" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isRemoved(f.browsers.chrome.status.text)]]">
|
||||
<span class="tooltip" title="Removed">
|
||||
<iron-icon icon="chromestatus:cancel" class="remove" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isDeprecated(f.browsers.chrome.status.text)]]">
|
||||
<span class="tooltip" title="Deprecated">
|
||||
<iron-icon icon="chromestatus:warning" class="deprecated" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span class="tooltip no-push-notifications" title="Subscribe to notification updates">
|
||||
<iron-icon icon="chromestatus:notifications-off"
|
||||
on-click="subscribeToFeature" class$="pushicon [[pushDisabled()]]"></iron-icon>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section class="release">
|
||||
<div class="layout vertical center">
|
||||
<h1 class="channel_label"> </h1>
|
||||
<h1 class="chrome_version chrome_version--dev layout horizontal center">
|
||||
<span class="chrome-logo"></span>
|
||||
<a href="https://www.google.com/chrome/browser/canary.html" target="_blank" title="Download Chrome Canary">Chrome [[channels.dev.version]]</a>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="milestone_info layout horizontal center-center">
|
||||
<h3><span class="channel_label">Beta </span> coming <span class="milestone_info-beta">[[_computeDate(channels.dev.earliest_beta)]] - [[_computeDate(channels.dev.latest_beta)]]</span></h3>
|
||||
</div>
|
||||
<div class="milestone_info layout horizontal center-center">
|
||||
<h3><span class="channel_label">Stable</span> [[_computeDaysUntil(channels.dev.stable_date)]]
|
||||
<span class="release-stable">( [[_computeDate(channels.dev.stable_date)]] )</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="features_list">
|
||||
<div class="features_header">Features planned in this release:</div>
|
||||
|
||||
<template is="dom-repeat" items="[[_objKeys(channels.dev.components)]]" as="componentName">
|
||||
<h3 class="feature_components">{{componentName}}</h3>
|
||||
<ul>
|
||||
<template is="dom-repeat" items="[[_featuresFor(channels.dev.components, componentName)]]" as="f">
|
||||
<li data-feature-id$="[[f.id]]">
|
||||
<a href$="/feature/[[f.id]]">[[f.name]]</a>
|
||||
<span class="icon_row">
|
||||
<template is="dom-if" if="[[f.browsers.chrome.origintrial]]">
|
||||
<span class="tooltip" title="Origin Trial">
|
||||
<iron-icon icon="chromestatus:extension" class="experimental" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[f.browsers.chrome.intervention]]">
|
||||
<span class="tooltip" title="Browser intervention">
|
||||
<iron-icon icon="chromestatus:pan-tool" class="intervention" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isRemoved(f.browsers.chrome.status.text)]]">
|
||||
<span class="tooltip" title="Removed">
|
||||
<iron-icon icon="chromestatus:cancel" class="remove" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<template is="dom-if" if="[[isDeprecated(f.browsers.chrome.status.text)]]">
|
||||
<span class="tooltip" title="Deprecated">
|
||||
<iron-icon icon="chromestatus:warning" class="deprecated" data-tooltip></iron-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span class="tooltip no-push-notifications" title="Subscribe to notification updates">
|
||||
<iron-icon icon="chromestatus:notifications-off"
|
||||
on-click="subscribeToFeature" class$="pushicon [[pushDisabled()]]"></iron-icon>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-schedule', ChromedashSchedule);
|
|
@ -1,218 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
import 'https://www.gstatic.com/charts/loader.js'; // Global `google.charts.load`
|
||||
|
||||
class ChromedashTimeline extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
selectedBucketId: {type: Number},
|
||||
showAllHistoricalData: {type: Boolean},
|
||||
timeline: {type: Object}, // Not used?
|
||||
title: {type: String},
|
||||
type: {type: String},
|
||||
view: {type: String},
|
||||
props: {type: Array}, // Directly edited from metrics/css/timeline/popularity and metrics/feature/timeline/popularity
|
||||
useRemoteData: {type: Boolean}, // If true, fetches live data from chromestatus.com instead of localhost.
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selectedBucketId = 1;
|
||||
this.showAllHistoricalData = false;
|
||||
this.title = '';
|
||||
this.type = '';
|
||||
this.view = '';
|
||||
this.props = [];
|
||||
this.useRemoteData = false;
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
const TRACKING_PROPERTIES = [
|
||||
'selectedBucketId',
|
||||
'type',
|
||||
'view',
|
||||
'useRemoteData',
|
||||
'showAllHistoricalData',
|
||||
];
|
||||
if (TRACKING_PROPERTIES.some((property) => changedProperties.get(property))) {
|
||||
this._updateTimeline();
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedBucketId(e) {
|
||||
this.selectedBucketId = e.currentTarget.value;
|
||||
}
|
||||
|
||||
toggleShowAllHistoricalData() {
|
||||
this.showAllHistoricalData = !this.showAllHistoricalData;
|
||||
}
|
||||
|
||||
ready() {
|
||||
google.charts.load('current', {'packages': ['corechart']});
|
||||
google.charts.setOnLoadCallback(() => {
|
||||
// If there's an id in the URL, load the property it.
|
||||
const lastSlash = location.pathname.lastIndexOf('/');
|
||||
if (lastSlash > 0) {
|
||||
const id = parseInt(location.pathname.substring(lastSlash + 1));
|
||||
if (String(id) != 'NaN') {
|
||||
this.selectedBucketId = id;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawVisualization(data, bucketId, showAllHistoricalData) {
|
||||
const table = new google.visualization.DataTable();
|
||||
table.addColumn('date', 'Date');
|
||||
table.addColumn('number', 'Percentage');
|
||||
table.addColumn({type: 'string', role: 'annotation'});
|
||||
table.addColumn({type: 'string', role: 'annotationText'});
|
||||
|
||||
const rowArray = [];
|
||||
for (let i = 0, item; item = data[i]; ++i) {
|
||||
const dateStr = item.date.split('-');
|
||||
const date = new Date();
|
||||
date.setFullYear(parseInt(dateStr[0]), parseInt(dateStr[1]) - 1, parseInt(dateStr[2]));
|
||||
const row = [date, parseFloat((item.day_percentage * 100).toFixed(6))];
|
||||
// Add annotation where UMA data switched to new stuff.
|
||||
if (item.date === '2017-10-27') {
|
||||
row.push('A', 'Modernized metrics');
|
||||
} else {
|
||||
row.push(null, null);
|
||||
}
|
||||
rowArray.push(row);
|
||||
}
|
||||
|
||||
table.addRows(rowArray);
|
||||
|
||||
table = google.visualization.data.group(table,
|
||||
[{column: 0, modifier: new Date(date.getFullYear(), date.getMonth()), type: 'date'}],
|
||||
[{
|
||||
column: 1,
|
||||
aggregation: google.visualization.data.avg,
|
||||
type: 'number',
|
||||
label: 'Monthly Average',
|
||||
}],
|
||||
[{column: 2, type: 'string'}]
|
||||
);
|
||||
|
||||
const formatter = new google.visualization.NumberFormat({fractionDigits: 6});
|
||||
formatter.format(table, 1); // Apply formatter to percentage column.
|
||||
|
||||
let view = table;
|
||||
|
||||
if (!showAllHistoricalData) {
|
||||
const startYear = (new Date()).getFullYear() - 2; // Show only 2 years of data by default.
|
||||
view = new google.visualization.DataView(table);
|
||||
view.setRows(view.getFilteredRows([{column: 0, minValue: new Date(startYear, 0, 1)}]));
|
||||
}
|
||||
|
||||
const chart = new google.visualization.LineChart(this.$.chart);
|
||||
chart.draw(view, {
|
||||
// title: this.title,
|
||||
// subtitle: 'all channels and platforms',
|
||||
backgroundColor: 'white',
|
||||
legend: {position: 'none'},
|
||||
curveType: 'function',
|
||||
vAxis: {
|
||||
title: '% page loads',
|
||||
// maxValue: 100,
|
||||
minValue: 0,
|
||||
},
|
||||
hAxis: {
|
||||
title: 'Date',
|
||||
format: 'M/yy',
|
||||
},
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointSize: 4,
|
||||
series: {0: {color: '#4580c0'}},
|
||||
trendlines: {
|
||||
0: {
|
||||
type: 'linear',
|
||||
opacity: 0.5,
|
||||
color: '#bdd6ed',
|
||||
pointsVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_updateTimeline() {
|
||||
if (this.selectedBucketId === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = this.useRemoteData ? 'https://www.chromestatus.com' : '';
|
||||
|
||||
const url = prefix + '/data/timeline/' + this.type + this.view +
|
||||
'?bucket_id=' + this.selectedBucketId;
|
||||
|
||||
this._renderHTTPArchiveData();
|
||||
|
||||
fetch(url).then((res) => res.json()).then((response) => {
|
||||
this.drawVisualization(response, this.selectedBucketId, this.showAllHistoricalData);
|
||||
});
|
||||
|
||||
if (history.pushState) {
|
||||
const url = '/metrics/' + this.type + '/timeline/' + this.view + '/' +
|
||||
this.selectedBucketId;
|
||||
history.pushState({id: this.selectedBucketId}, '', url);
|
||||
}
|
||||
}
|
||||
|
||||
_renderHTTPArchiveData() {
|
||||
if (!this.props.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const feature = this.props.find((el) => el[0] === parseInt(this.selectedBucketId));
|
||||
if (feature) {
|
||||
const featureName = feature[1];
|
||||
const REPORT_ID = '1M8kXOqPkwYNKjJhtag_nvDNJCpvmw_ri';
|
||||
const dsEmbedUrl = `https://datastudio.google.com/embed/reporting/${REPORT_ID}/page/tc5b?config=%7B"df3":"include%25EE%2580%25800%25EE%2580%2580IN%25EE%2580%2580${featureName}"%7D`;
|
||||
this.$.httparchivedata.src = dsEmbedUrl;
|
||||
|
||||
this.$.bigquery.textContent = `#standardSQL
|
||||
SELECT yyyymmdd, client, pct_urls, sample_urls
|
||||
FROM \`httparchive.blink_features.usage\`
|
||||
WHERE feature = '${featureName}'
|
||||
ORDER BY yyyymmdd DESC, client`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/css/elements/chromedash-timeline.css">
|
||||
|
||||
<select .value="${this.selectedBucketId}" @change="${this.updateSelectedBucketId}">
|
||||
<option disabled value="1">Select a property</option>
|
||||
${this.props.map((prop) => html`
|
||||
<option value="${prop[0]}">${prop[1]}</option>
|
||||
`)}
|
||||
</select>
|
||||
<label>Show all historical data: <input type="checkbox" ?checked="${this.showAllHistoricalData}" @change="${this.toggleShowAllHistoricalData}"></label>
|
||||
<h3 id="usage" class="header_title">Percentage of page loads that use this feature</h3>
|
||||
<p class="description">The chart below shows the percentage of page loads (in Chrome) that use
|
||||
this feature at least once. Data is across all channels and platforms.
|
||||
</p>
|
||||
<div id="chart"></div>
|
||||
<p class="callout">
|
||||
<b>Note</b>: on 2017-10-26 the underlying metrics were switched over to a newer collection system
|
||||
which is <a href="https://groups.google.com/a/chromium.org/forum/#!msg/blink-api-owners-discuss/IpIkbz0qtrM/HUCfSMv2AQAJ" target="_blank">more accurate</a>.
|
||||
This is also the reason for the abrupt spike around 2017-10-26.
|
||||
</p>
|
||||
<h3 id="httparchive" class="header_title">Adoption of the feature on top sites</h3>
|
||||
<p class="description">The chart below shows the adoption of the feature by the top URLs on the internet. Data from <a href="https://httparchive.org/" target="blank">HTTP Archive</a>.</p>
|
||||
<iframe id="httparchivedata"></iframe>
|
||||
<p class="callout">
|
||||
<b>Note</b>: The jump around July and December 2018 are because the corpus of URLs crawled by HTTP Archive increased. These jumps have no correlation with the jump in the top graph.
|
||||
See the <a href="https://discuss.httparchive.org/t/changes-to-the-http-archive-corpus/1539" target="_blank">announcement</a> for more details.
|
||||
</p>
|
||||
<p class="description">Copy and run this command in <a href="https://bigquery.cloud.google.com" target="_blank">BigQuery</a> to produce similar results:</p>
|
||||
<pre id="bigquery"></pre>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-timeline', ChromedashTimeline);
|
|
@ -1,88 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
|
||||
// Keeps track of the toast currently opened.
|
||||
let _currentToast = null;
|
||||
|
||||
class ChromedashToast extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
msg: {type: String}, // The toast's message.
|
||||
actionLabel: {type: String}, // A label for the call to action of the toast.
|
||||
duration: {type: Number}, // The duration in milliseconds to show the toast. -1 or `Infinity`, to disable the toast auto-closing.
|
||||
open: {type: Boolean, reflect: true},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.msg = '';
|
||||
this.actionLabel = '';
|
||||
this.duration = 7000;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message in the toast.
|
||||
* @method showMessage
|
||||
* @param {string} msg Message to show.Notification (event) type.
|
||||
* @param {string} optAction Optional action link.
|
||||
* @param {function} optTapHandler Optional handler to execute when the
|
||||
* toast action is tapped.
|
||||
* @param {Number} optDuration Optional duration to show the toast for.
|
||||
*/
|
||||
showMessage(msg, optAction, optTapHandler, optDuration) {
|
||||
this.msg = msg;
|
||||
this.actionLabel = optAction;
|
||||
this.shadowRoot.querySelector('#action').addEventListener('click', () => {
|
||||
e.preventDefault();
|
||||
if (optTapHandler) {
|
||||
optTapHandler();
|
||||
}
|
||||
}, {once: true});
|
||||
|
||||
// Override duration just for this toast.
|
||||
const originalDuration = this.duration;
|
||||
if (typeof optDuration !== 'undefined') {
|
||||
this.duration = optDuration;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
this.duration = originalDuration; // reset site-wide duration.
|
||||
}
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_openChanged() {
|
||||
clearTimeout(this._timerId);
|
||||
|
||||
if (this.open) {
|
||||
// Close existing toast if one is showing.
|
||||
if (_currentToast && _currentToast !== this) {
|
||||
_currentToast.close();
|
||||
}
|
||||
_currentToast = this;
|
||||
|
||||
if (this.duration >= 0) {
|
||||
this._timerId = setTimeout(() => this.close(), this.duration);
|
||||
}
|
||||
} else if (_currentToast === this) {
|
||||
_currentToast = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/css/elements/chromedash-toast.css">
|
||||
|
||||
<div id="message_container">
|
||||
<span id="msg">${this.msg}</span>
|
||||
<a href="#" id="action">${this.actionLabel}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-toast', ChromedashToast);
|
|
@ -1,105 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
|
||||
class ChromedashUserlist extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
action: {type: String},
|
||||
users: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.users = [];
|
||||
}
|
||||
|
||||
addUser(user) {
|
||||
this.splice('users', 0, 0, user);
|
||||
}
|
||||
|
||||
removeUser(idx) {
|
||||
return this.splice('users', idx, 1);
|
||||
}
|
||||
|
||||
ajaxSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.$.form.checkValidity()) {
|
||||
// TODO(ericbidelman): move back to this.$.form.email.value when SD
|
||||
// polyfill merges the commit that wraps element.form.
|
||||
const email = this.$.form.querySelector('input[name="email"]').value;
|
||||
const formData = new FormData();
|
||||
formData.append('email', email);
|
||||
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin', // Needed for admin permissions to be sent.
|
||||
}).then((resp) => {
|
||||
if (resp.status === 200) {
|
||||
alert('Thanks. But that user already exists');
|
||||
throw new Error('User already added');
|
||||
} else if (resp.status !== 201) {
|
||||
throw new Error('Sever error adding new user');
|
||||
}
|
||||
|
||||
return resp.json();
|
||||
}).then((json) => {
|
||||
this.addUser(json);
|
||||
this.$.form.reset();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ajaxDelete(e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('Remove user?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get index of user model instance that was clicked from template.
|
||||
const user = e.model.user;
|
||||
const idx = this.users.indexOf(user);
|
||||
|
||||
fetch(e.currentTarget.href, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
}).then(() => {
|
||||
const liEl = e.currentTarget.parentElement;
|
||||
liEl.classList.add('faded');
|
||||
if ('ontransitionend' in window) {
|
||||
liEl.addEventListener('transitionend', () => {
|
||||
this.removeUser(idx);
|
||||
});
|
||||
liEl.classList.add('faded');
|
||||
} else {
|
||||
this.removeUser(idx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/css/elements/chromedash-userlist.css">
|
||||
|
||||
<form id="form" name="user_form" method="post" action="${this.action}" onsubmit="return false;">
|
||||
<table>
|
||||
<tr>
|
||||
<td><input type="email" placeholder="Email address" name="email" id="id_email" required></td>
|
||||
<td><input type="submit" @click="${this.ajaxSubmit}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<ul id="user-list">
|
||||
${this.users.map(user => html`
|
||||
<li>
|
||||
<a href="${this.action}/${user.id}" @click="${this.ajaxDelete}">delete</a>
|
||||
<span>${user.email}</span>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-userlist', ChromedashUserlist);
|
|
@ -1,39 +0,0 @@
|
|||
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
|
||||
|
||||
class ChromedashXMeter extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
value: {type: Number},
|
||||
valueFormatted: {type: Number},
|
||||
max: {type: Number}, // Normalized, maximum width for the bar
|
||||
};
|
||||
}
|
||||
|
||||
get valueFormatted() {
|
||||
return this.valueFormatted_;
|
||||
}
|
||||
|
||||
set valueFormatted(value) {
|
||||
this.valueFormatted_ = value <= 0.000001 ? '<=0.000001%' : value + '%';
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.value = 0;
|
||||
this.max = 100;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link rel="stylesheet" href="/static/css/chromedash-x-meter.css">
|
||||
|
||||
<div class="meter" id="meter">
|
||||
<div style="width: ${(this.value / this.max * 100)}%">
|
||||
<span>${this.valueFormatted}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chromedash-x-meter', ChromedashXMeter);
|
Загрузка…
Ссылка в новой задаче