Keep features up-to-date in the Feature Detail page (#4481)

* Feature detail page UI for keeping upcoming milestones up-to-date
This commit is contained in:
Kyle Ju 2024-10-30 12:33:49 -07:00 коммит произвёл GitHub
Родитель b4c5cedb5f
Коммит 59a1e11bee
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 247 добавлений и 3 удалений

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

@ -7,13 +7,15 @@ import {
FeatureLink,
FeatureNotFoundError,
User,
StageDict,
} from '../js-src/cs-client.js';
import './chromedash-feature-detail';
import {DETAILS_STYLES} from './chromedash-feature-detail';
import './chromedash-feature-highlights.js';
import {GateDict} from './chromedash-gate-chip.js';
import {Process, ProgressItem} from './chromedash-gate-column.js';
import {showToastMessage} from './utils.js';
import {showToastMessage, isVerifiedWithinGracePeriod} from './utils.js';
import {STAGE_TYPES_SHIPPING} from './form-field-enums';
const INACTIVE_STATES = ['No longer pursuing', 'Deprecated', 'Removed'];
declare var ga: Function;
@ -109,6 +111,13 @@ export class ChromedashFeaturePage extends LitElement {
starred = false;
@state()
loading = true;
@state()
isUpcoming = false;
@state()
currentDate: number = Date.now();
@state()
// The closest milestone shipping date as an ISO string.
closestShippingDate: string = '';
connectedCallback() {
super.connectedCallback();
@ -119,6 +128,60 @@ export class ChromedashFeaturePage extends LitElement {
return this.feature && Object.keys(this.feature).length !== 0;
}
/**
* Determine if this feature is upcoming - scheduled to ship
* within two milestones, and find the closest shipping date
* for that milestone.*/
calcUpcoming(channels, stages: Array<StageDict>) {
const latestStableVersion = channels['stable']?.version;
if (!latestStableVersion || !stages) {
return;
}
const shippingMilestones = new Set<number | undefined>();
// Get milestones from all shipping stages, STAGE_TYPES_SHIPPING.
for (const stage of stages) {
if (STAGE_TYPES_SHIPPING.has(stage.stage_type)) {
shippingMilestones.add(stage.desktop_first);
shippingMilestones.add(stage.android_first);
shippingMilestones.add(stage.ios_first);
shippingMilestones.add(stage.webview_first);
}
}
// Check if this feature is shipped within two milestones.
let foundMilestone = 0;
if (shippingMilestones.has(latestStableVersion + 1)) {
foundMilestone = latestStableVersion + 1;
this.isUpcoming = true;
} else if (shippingMilestones.has(latestStableVersion + 2)) {
foundMilestone = latestStableVersion + 2;
this.isUpcoming = true;
}
if (this.isUpcoming) {
Object.keys(channels).forEach(key => {
if (channels[key].version === foundMilestone) {
this.closestShippingDate = channels[key].final_beta;
}
});
}
}
/**
* A feature is outdated if it is scheduled to ship in the next 2 milestones,
* and its accurate_as_of date is at least 4 weeks ago.*/
isFeatureOutdated(): boolean {
if (!this.isUpcoming) {
return false;
}
const isVerified = isVerifiedWithinGracePeriod(
this.feature.accurate_as_of,
this.currentDate
);
return !isVerified;
}
fetchData() {
this.loading = true;
Promise.all([
@ -128,6 +191,7 @@ export class ChromedashFeaturePage extends LitElement {
window.csClient.getFeatureProcess(this.featureId),
window.csClient.getStars(),
window.csClient.getFeatureProgress(this.featureId),
window.csClient.getChannels(),
])
.then(
([
@ -137,6 +201,7 @@ export class ChromedashFeaturePage extends LitElement {
process,
starredFeatures,
progress,
channels,
]) => {
this.feature = feature;
this.gates = gatesRes.gates;
@ -149,6 +214,7 @@ export class ChromedashFeaturePage extends LitElement {
if (this.feature.name) {
document.title = `${this.feature.name} - ${this.appTitle}`;
}
this.calcUpcoming(channels, feature.stages);
this.loading = false;
}
)
@ -420,6 +486,50 @@ export class ChromedashFeaturePage extends LitElement {
</div>
`);
}
if (this.isFeatureOutdated()) {
if (this.userCanEdit()) {
warnings.push(html`
<div class="warning layout horizontal center">
<span class="tooltip" id="outdated-icon" title="Feature outdated ">
<iron-icon icon="chromestatus:error" data-tooltip></iron-icon>
</span>
<span>
Your feature hasn't been verified as accurate since${' '}
<sl-relative-time
date=${this.feature.accurate_as_of}
></sl-relative-time
>, but it is scheduled to ship${' '}
<sl-relative-time
date=${this.closestShippingDate}
></sl-relative-time
>. Please
<a href="/guide/verify_accuracy/${this.featureId}"
>verify that your feature is accurate</a
>.
</span>
</div>
`);
} else {
warnings.push(html`
<div class="warning layout horizontal center">
<span class="tooltip" id="outdated-icon" title="Feature outdated ">
<iron-icon icon="chromestatus:error" data-tooltip></iron-icon>
</span>
<span>
This feature hasn't been verified as accurate since${' '}
<sl-relative-time
date=${this.feature.accurate_as_of}
></sl-relative-time
>, but it is scheduled to ship${' '}
<sl-relative-time
date=${this.closestShippingDate}
></sl-relative-time
>.
</span>
</div>
`);
}
}
return warnings;
}

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

@ -39,7 +39,7 @@ describe('chromedash-feature-page', () => {
'Spec link': 'fake spec link',
'Web developer signals': 'True',
});
const channelsPromise = Promise.resolve({
const channels = {
canary_asan: {
version: 81,
earliest_beta: '2020-02-13T00:00:00',
@ -59,13 +59,15 @@ describe('chromedash-feature-page', () => {
version: 80,
earliest_beta: '2020-02-13T00:00:00',
mstone: 'fake milestone number',
final_beta: '2020-03-13T00:00:00',
},
stable: {
version: 79,
earliest_beta: '2020-02-13T00:00:00',
mstone: 'fake milestone number',
},
});
};
const channelsPromise = Promise.resolve(channels);
const validFeaturePromise = Promise.resolve({
id: 123456,
name: 'feature one',
@ -103,6 +105,12 @@ describe('chromedash-feature-page', () => {
stage_type: 120,
intent_stage: 2,
},
{
id: 3,
stage_type: 160,
intent_stage: 3,
desktop_first: 80,
},
],
});
@ -314,4 +322,101 @@ describe('chromedash-feature-page', () => {
// But it does still include webdev views.
assert.include(consensusSection.innerHTML, 'fake webdev view text');
});
it('calcUpcoming() tests', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.user=${user}
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
assert.exists(component);
component.calcUpcoming({}, feature.stages);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
component.calcUpcoming(channels, []);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
// No shipping milestones.
let stages: any = structuredClone(feature.stages);
stages[2].stage_type = 130;
component.calcUpcoming(channels, stages);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
// No upcoming shipping milestones.
stages = structuredClone(feature.stages);
stages[2].desktop_first = 20;
component.calcUpcoming(channels, stages);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
component.calcUpcoming(channels, feature.stages);
assert.isTrue(component.isUpcoming);
assert.equal(component.closestShippingDate, '2020-03-13T00:00:00');
});
it('isFeatureOutdated() tests', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
feature.accurate_as_of = '2024-08-28 21:51:34.22386';
window.csClient.getFeature
.withArgs(featureId)
.returns(Promise.resolve(feature));
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.user=${user}
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
component.currentDate = new Date('2024-10-23').getTime();
assert.exists(component);
component.calcUpcoming(channels, feature.stages);
assert.isTrue(component.isUpcoming);
assert.equal(component.closestShippingDate, '2020-03-13T00:00:00');
assert.isTrue(component.isFeatureOutdated());
// accurate_as_of is not outdated and within the 4-week grace period.
component.currentDate = new Date('2024-09-18').getTime();
assert.isFalse(component.isFeatureOutdated());
});
it('render the oudated warning when outdated', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
feature.accurate_as_of = '2024-08-28 21:51:34.22386';
window.csClient.getFeature
.withArgs(featureId)
.returns(Promise.resolve(feature));
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.user=${user}
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
component.currentDate = new Date('2024-10-23').getTime();
assert.exists(component);
component.calcUpcoming(channels, feature.stages);
const oudated = component.shadowRoot!.querySelector('#outdated-icon');
assert.exists(oudated);
});
});

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

@ -21,6 +21,10 @@ let toastEl;
// We assume that a small enough window width implies a mobile device.
const NARROW_WINDOW_MAX_WIDTH = 700;
// Represent a 4-week period in milliseconds. This grace period needs
// to be consistent with ACCURACY_GRACE_PERIOD in internals/reminders.py.
const ACCURACY_GRACE_PERIOD = 4 * 7 * 24 * 60 * 60 * 1000;
export const IS_MOBILE = (() => {
const width =
window.innerWidth ||
@ -652,3 +656,26 @@ export function extensionMilestoneIsValid(value, currentMilestone) {
// End milestone should not be in the past.
return parseInt(currentMilestone) <= intValue;
}
/**
* Check if feature.accurate_as_of is verified, within the four-week
* grace period to currentDate.
*
* @param accurateAsOf The accurate_as_of date as an ISO string.
* @param currentDate The current date in milliseconds.
*/
export function isVerifiedWithinGracePeriod(
accurateAsOf: string | undefined,
currentDate: number
) {
if (!accurateAsOf) {
return false;
}
const accurateDate = Date.parse(accurateAsOf);
if (accurateDate + ACCURACY_GRACE_PERIOD < currentDate) {
return false;
}
return true;
}

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

@ -220,6 +220,8 @@ class AbstractReminderHandler(basehandlers.FlaskHandler):
class FeatureAccuracyHandler(AbstractReminderHandler):
"""Periodically remind owners to verify the accuracy of their entries."""
# This grace period needs to be consistent with
# ACCURACY_GRACE_PERIOD in client-src/elements/utils.ts.
ACCURACY_GRACE_PERIOD = timedelta(weeks=4)
SUBJECT_FORMAT = '[Action requested] Update %s'
EMAIL_TEMPLATE_PATH = 'accuracy_notice_email.html'