import {LitElement, css, html, nothing} from 'lit'; import {property, state} from 'lit/decorators.js'; import {ref} from 'lit/directives/ref.js'; import {FORM_STYLES} from '../css/forms-css.js'; import {SHARED_STYLES} from '../css/shared-css.js'; import {Feature, StageDict} from '../js-src/cs-client.js'; import './chromedash-form-field.js'; import './chromedash-form-table.js'; import {dialogTypes, openInfoDialog} from './chromedash-ot-prereqs-dialog'; import {ORIGIN_TRIAL_EXTENSION_FIELDS} from './form-definition.js'; import {OT_EXTENSION_STAGE_MAPPING} from './form-field-enums.js'; import {ALL_FIELDS} from './form-field-specs'; import { FieldInfo, extensionMilestoneIsValid, formatFeatureChanges, getDisabledHelpText, setupScrollToHash, showToastMessage, } from './utils.js'; export class ChromedashOTExtensionPage extends LitElement { static get styles() { return [...SHARED_STYLES, ...FORM_STYLES, css``]; } static get properties() { return { stageId: {type: Number}, featureId: {type: Number}, userEmail: {type: String}, feature: {type: Object}, loading: {type: Boolean}, appTitle: {type: String}, fieldValues: {type: Array}, // The most recent Chrome milestone. currentMilestone: {type: Number}, // A reference of end dates for an origin trial based on the milestone. // (key=milestone, value=date origin trial will end) endMilestoneDateValues: {type: Object}, }; } @property({type: Number}) stageId = 0; @property({type: Number}) featureId = 0; @property({type: String}) userEmail!: string; @property({type: String}) appTitle = ''; @state() feature!: Feature; @state() loading = true; @state() fieldValues: FieldInfo[] & {feature?: Feature} = []; @state() currentMilestone = 123; @state() endMilestoneDateValues: Record = {}; @state() stage!: StageDict; connectedCallback() { super.connectedCallback(); this.fetchData(); } // Handler to update form values when a field update event is fired. handleFormFieldUpdate(event) { const value = event.detail.value; // Index represents which form was updated. const index = event.detail.index; if (index >= this.fieldValues.length) { throw new Error('Out of bounds index when updating field values.'); } // The field has been updated, so it is considered touched. this.fieldValues[index].touched = true; this.fieldValues[index].value = value; if ( this.fieldValues[index].name == 'ot_extension__milestone_desktop_last' ) { this.getChromeScheduleDate(event.detail.value); } } openMilestoneExplanationDialog() { openInfoDialog(dialogTypes.END_MILESTONE_EXPLANATION); } // Display the date the origin trial will end to the user after a milestone is chosen. updateMilestoneDate(milestone) { const milestoneDiv: HTMLElement | null = this.renderRoot.querySelector('#milestone-date'); const milestoneTextEl: HTMLTextAreaElement | null = this.renderRoot.querySelector('#milestone-date-text'); const date = new Date(this.endMilestoneDateValues[milestone]); if (!milestoneDiv || !milestoneTextEl) { return; } milestoneDiv.style.display = 'block'; milestoneTextEl.innerHTML = `For milestone ${milestone}, this trial will end on ${date.toLocaleDateString()}.`; } // Obtain the date the origin trial will end based on the given milestone. async getChromeScheduleDate(milestone) { const milestoneDiv: HTMLElement | null = this.renderRoot.querySelector('#milestone-date'); if (!milestoneDiv) { return; } milestoneDiv.style.display = 'none'; // Don't try to obtain a date if the milestone is not valid. if (!extensionMilestoneIsValid(milestone, this.currentMilestone)) { return; } if (!(milestone in this.endMilestoneDateValues)) { // Origin trials will end on the late stable date of (milestone + 2). const milestonePlusTwo = parseInt(milestone) + 2; const resp = await fetch( `https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=${milestonePlusTwo}` ); const respJson = await resp.json(); // Keep a reference of milestone dates to avoid extra requests. this.endMilestoneDateValues[milestone] = respJson.mstones[0].late_stable_date; } this.updateMilestoneDate(milestone); } fetchData() { this.loading = true; Promise.all([ window.csClient.getFeature(this.featureId), window.csClient.getStage(this.featureId, this.stageId), ]) .then(([feature, stage]) => { this.feature = feature; this.stage = stage; if (this.feature.name) { document.title = `${this.feature.name} - ${this.appTitle}`; } this.loading = false; }) .catch(() => { showToastMessage( 'Some errors occurred. Please refresh the page or try again later.' ); }); // Fetch the current milestone so that we know if a milestone in the past is given. fetch('https://chromiumdash.appspot.com/fetch_milestone_schedule') .then(resp => resp.json()) .then(scheduleInfo => { this.currentMilestone = parseInt(scheduleInfo.mstones[0].mstone); }); } disconnectedCallback() { super.disconnectedCallback(); document.title = this.appTitle; } async registerHandlers(el) { if (!el) return; /* Add the form's event listener after Shoelace event listeners are attached * see more at https://github.com/GoogleChrome/chromium-dashboard/issues/2014 */ await el.updateComplete; const submitButton: HTMLInputElement | null = this.renderRoot.querySelector( 'input[id=submit-button]' ); submitButton?.form?.addEventListener('submit', event => { this.handleFormSubmit(event); }); setupScrollToHash(this); } handleFormSubmit(e) { e.preventDefault(); const featureSubmitBody = formatFeatureChanges( this.fieldValues, this.featureId ); // We only need the single stage changes. const stageSubmitBody = featureSubmitBody.stages[0]; let newStageId = null; window.csClient .createStage(this.featureId, stageSubmitBody) .then(resp => { newStageId = resp.stage_id; return window.csClient.getGates(this.featureId); }) .then(resp => { const gate = resp.gates.find(gate => gate.stage_id === newStageId); showToastMessage('Extension request started!'); if (!newStageId || !gate) { setTimeout(() => { window.location.href = `/feature/${this.featureId}`; }, 1000); } else { setTimeout(() => { window.location.href = `/feature/${this.featureId}?gate=${gate.id}`; }, 1000); } }) .catch(() => { showToastMessage( 'Some errors occurred. Please refresh the page or try again later.' ); }); } handleCancelClick(e) { e.preventDefault(); // Stops the form from being submitted. window.location.href = `/feature/${this.featureId}`; } renderSkeletons() { return html`

`; } getNextPage() { return `/feature/${this.featureId}`; } renderSubheader() { const link = this.loading ? nothing : html` Request origin trial extension: ${this.feature.name} `; return html`
`; } // Add a set of field information that will be sent with a request submission. // These fields are always considered touched, and are not visible to the user. addDefaultRequestFields() { // Add "ot_owner_email" field to represent the requester email. this.fieldValues.push({ name: 'ot_owner_email', touched: true, value: this.userEmail, stageId: this.stage.id, }); // Add "ot_owner_email" field to represent the requester email. this.fieldValues.push({ name: 'ot_action_requested', touched: true, value: true, stageId: this.stage.id, }); // Add "stage_type" field to create extension stage properly. const extensionStageType = OT_EXTENSION_STAGE_MAPPING[this.stage.stage_type]; this.fieldValues.push({ name: 'stage_type', touched: true, value: extensionStageType, stageId: this.stage.id, }); // Add "ot_stage_id" field to link extension stage to OT stage. this.fieldValues.push({ name: 'ot_stage_id', touched: true, value: this.stage.id, stageId: this.stage.id, }); } renderFields(section) { const fields = section.fields.map(field => { const featureJSONKey = ALL_FIELDS[field].name || field; // Add the field to this component's stage before creating the field component. const index = this.fieldValues.length; this.fieldValues.push({ name: featureJSONKey, touched: true, value: null, stageId: this.stage.id, }); // Add the extra elements to display milestone date information. let milestoneInfoText = html``; if (featureJSONKey === 'ot_extension__milestone_desktop_last') { milestoneInfoText = html` `; } return html` ${milestoneInfoText} `; }); // Add additional default hidden fields. this.addDefaultRequestFields(); return fields; } renderForm() { this.fieldValues.feature = this.feature; // OT extension page only has one section. const section = ORIGIN_TRIAL_EXTENSION_FIELDS.sections[0]; return html`
${this.renderFields(section)}
`; } render() { return html` ${this.renderSubheader()} ${this.loading ? this.renderSkeletons() : this.renderForm()} `; } } customElements.define( 'chromedash-ot-extension-page', ChromedashOTExtensionPage );