Implement the layout and some details for the "gate column" (#2583)

* progress

* progress

* Fill in stage and gate name.

* Update client-src/elements/chromedash-gate-column.js

Co-authored-by: Kyle Ju <kyleju@google.com>

* Update client-src/elements/chromedash-gate-column.js

Co-authored-by: Kyle Ju <kyleju@google.com>

* Implement handleCancel and make as wide as mocks.

* Improved indentation

Co-authored-by: Kyle Ju <kyleju@google.com>
This commit is contained in:
Jason Robbins 2022-12-15 10:44:49 -08:00 коммит произвёл GitHub
Родитель 9001b06bdf
Коммит cb1a07ad25
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 359 добавлений и 16 удалений

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

@ -58,6 +58,7 @@ import './elements/chromedash-footer';
import './elements/chromedash-form-field';
import './elements/chromedash-form-table';
import './elements/chromedash-gate-chip';
import './elements/chromedash-gate-column';
import './elements/chromedash-gantt';
import './elements/chromedash-guide-edit-page';
import './elements/chromedash-guide-editall-page';

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

@ -1,10 +1,13 @@
import {LitElement, css, html, nothing} from 'lit';
import {ref, createRef} from 'lit/directives/ref.js';
import {showToastMessage} from './utils';
import page from 'page';
import {SHARED_STYLES} from '../sass/shared-css.js';
class ChromedashApp extends LitElement {
gateColumnRef = createRef();
static get styles() {
return [
...SHARED_STYLES,
@ -51,6 +54,32 @@ class ChromedashApp extends LitElement {
width: 100%;
}
#content-sidebar-space {
position: sticky;
flex-shrink: 100;
width: var(--sidebar-space);
}
#sidebar {
position: absolute;
top: 0;
right: 0;
width: var(--sidebar-width);
bottom: 0;
}
#sidebar[hidden] {
display: none;
}
#sidebar-content {
position: sticky;
top: 10px;
height: 85vh;
border: var(--sidebar-border);
border-radius: var(--sidebar-radius);
background: var(--sidebar-bg);
padding: var(--content-padding);
}
@media only screen and (max-width: 700px) {
#content {
margin-left: 0;
@ -71,6 +100,7 @@ class ChromedashApp extends LitElement {
bannerTime: {type: Number},
pageComponent: {attribute: false},
contextLink: {type: String}, // used for the back button in the feature page
sidebarHidden: {type: Boolean},
};
}
@ -85,6 +115,7 @@ class ChromedashApp extends LitElement {
this.bannerTime = null;
this.pageComponent = null;
this.contextLink = '/features';
this.sidebarHidden = true;
}
connectedCallback() {
@ -205,6 +236,19 @@ class ChromedashApp extends LitElement {
this.updateURLParams('q', e.detail.query);
}
showSidebar() {
this.sidebarHidden = false;
}
hideSidebar() {
this.sidebarHidden = true;
}
showGateColumn(feature, stage, gate) {
this.gateColumnRef.value.setContext(feature, stage, gate);
this.showSidebar();
}
/**
* Update window.locaton with new query params.
* @param {string} key is the key of the query param.
@ -248,6 +292,35 @@ class ChromedashApp extends LitElement {
return url;
}
renderContentAndSidebar() {
const wide = (this.pageComponent &&
this.pageComponent.tagName == 'CHROMEDASH-ROADMAP-PAGE');
if (wide) {
return html`
<div id="content-component-wrapper" wide>
${this.pageComponent}
</div>
`;
} else {
return html`
<div id="content-component-wrapper">
${this.pageComponent}
</div>
<div id="content-sidebar-space">
<div id="sidebar" ?hidden=${this.sidebarHidden}>
<div id="sidebar-content">
<chromedash-gate-column
.user=${this.user} ${ref(this.gateColumnRef)}
@close=${() => this.hideSidebar()}>
</chromedash-gate-column>
</div>
</div>
</div>
`;
}
}
renderRolloutBanner(currentPage) {
if (currentPage.startsWith('/newfeatures')) {
return html`
@ -286,11 +359,7 @@ class ChromedashApp extends LitElement {
</chromedash-banner>
${this.renderRolloutBanner(this.currentPage)}
<div id="content-flex-wrapper">
<div id="content-component-wrapper"
?wide=${this.pageComponent &&
this.pageComponent.tagName == 'CHROMEDASH-ROADMAP-PAGE'}>
${this.pageComponent}
</div>
${this.renderContentAndSidebar()}
</div>
</div>

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

@ -5,7 +5,7 @@ import '@polymer/iron-icon';
import './chromedash-activity-log';
import './chromedash-callout';
import './chromedash-gate-chip';
import {autolink} from './utils.js';
import {autolink, findProcessStage} from './utils.js';
import {SHARED_STYLES} from '../sass/shared-css.js';
const LONG_TEXT = 60;
@ -328,17 +328,8 @@ class ChromedashFeatureDetail extends LitElement {
`;
}
findProcessStage(feStage) {
for (const processStage of this.process.stages) {
if (feStage.stage_type == processStage.stage_type) {
return processStage;
}
}
return null;
}
renderStageSection(feStage) {
const processStage = this.findProcessStage(feStage);
const processStage = findProcessStage(feStage, this.process);
if (processStage === null) return nothing;
const fields = DISPLAY_FIELDS_IN_STAGES[processStage.outgoing_stage];
const isActive = (this.feature.intent_stage_int == processStage.outgoing_stage);

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

@ -0,0 +1,265 @@
import {LitElement, css, html, nothing} from 'lit';
import './chromedash-activity-log';
import {showToastMessage, findProcessStage} from './utils.js';
import {SHARED_STYLES} from '../sass/shared-css.js';
export const STATE_NAMES = [
[7, 'No response'],
[1, 'N/a or Ack'],
[2, 'Review requested'],
[3, 'Review started'],
[4, 'Needs work'],
[5, 'Approved'],
[6, 'Denied'],
];
class ChromedashGateColumn extends LitElement {
static get styles() {
return [
...SHARED_STYLES,
css`
#votes-area {
margin: var(--content-padding) 0;
}
#votes-area table {
border-spacing: var(--content-padding-half) var(--content-padding);;
}
#votes-area th {
font-weight: bold;
}
#review-status-area {
margin: var(--content-padding-half) 0;
}
`];
}
static get properties() {
return {
user: {type: Object},
feature: {type: Object},
stage: {type: Object},
gate: {type: Object},
process: {type: Object},
votes: {type: Array},
comments: {type: Array},
loading: {type: Boolean},
needsSave: {type: Boolean},
};
}
constructor() {
super();
this.user = {};
this.feature = {};
this.stage = {};
this.gate = {};
this.process = {};
this.votes = [];
this.comments = [];
this.loading = false;
this.needsSave = false;
}
setContext(feature, stage, gate) {
this.loading = true;
this.feature = feature;
this.stage = stage;
this.gate = gate;
const featureId = this.feature.id;
Promise.all([
window.csClient.getFeatureProcess(featureId),
window.csClient.getApprovals(featureId),
window.csClient.getComments(featureId),
]).then(([process, approvalRes, commentRes]) => {
this.process = process;
this.votes = approvalRes.approvals.filter((v) =>
v.gate_id == this.gate.id);
this.comments = commentRes.comments;
this.needsSave = false;
this.loading = false;
}).catch(() => {
showToastMessage('Some errors occurred. Please refresh the page or try again later.');
this.handleCancel();
});
}
_fireEvent(eventName, detail) {
const event = new CustomEvent(eventName, {
bubbles: true,
composed: true,
detail,
});
this.dispatchEvent(event);
}
handleCancel() {
this._fireEvent('close', {});
}
renderHeadingsSkeleton() {
return html`
<h3 class="sl-skeleton-header-container">
<sl-skeleton effect="sheen"></sl-skeleton>
</h3>
<h2 class="sl-skeleton-header-container">
<sl-skeleton effect="sheen"></sl-skeleton>
</h2>
`;
}
renderHeadings() {
const processStage = findProcessStage(this.stage, this.process);
const processStageName = processStage ? processStage.name : nothing;
return html`
<h3>${processStageName}</h3>
<h2>${this.gate.team_name}</h2>
`;
}
renderReviewStatusSkeleton() {
return html`
<h3 class="sl-skeleton-header-container">
Status: <sl-skeleton effect="sheen"></sl-skeleton>
</h3>
`;
}
renderReviewStatus() {
// TODO(jrobbins): display gate state name and requested_on or reviewed_on.
return html`
<div>
Review requested on YYYY-MM-DD
</div>
`;
}
renderVotesSkeleton() {
return html`
<table>
<tr><th>Reviewer</th><th>Review status</th></tr>
<tr>
<td><sl-skeleton effect="sheen"></sl-skeleton></td>
<td><sl-skeleton effect="sheen"></sl-skeleton></td>
</tr>
</table>
`;
}
findStateName(state) {
for (const item of STATE_NAMES) {
if (item[0] == state) {
return item[1];
}
}
// This should not normally be seen by users, but it will help us
// cope with data migration.
return `State ${state}`;
}
renderVoteReadOnly(vote) {
// TODO(jrobbins): background colors
return this.findStateName(vote.state);
}
renderVoteMenu(state) {
// hoist is needed when <sl-select> is in overflow:hidden context.
return html`
<sl-select name="${this.gate.id}"
value="${state}"
@sl-change=${this.handleSelectChanged}
hoist size="small">
${STATE_NAMES.map((valName) => html`
<sl-menu-item value="${valName[0]}">${valName[1]}</sl-menu-item>`,
)}
</sl-select>
`;
}
renderVoteRow(vote) {
const voteCell = (vote.set_by == this.user.email) ?
this.renderVoteMenu(vote.state) :
this.renderVoteReadOnly(vote);
return html`
<tr>
<td>${vote.set_by}</td>
<td>${voteCell}</td>
</tr>
`;
}
renderVotes() {
const canVote = true; // TODO(jrobbins): permission checks.
const myVoteExists = this.votes.some((v) => v.set_by == this.user.email);
const addVoteRow = (canVote && !myVoteExists) ?
this.renderVoteRow({set_by: this.user.email, state: 7}) :
nothing;
return html`
<table>
<tr><th>Reviewer</th><th>Review status</th></tr>
${this.votes.map((v) => this.renderVoteRow(v))}
${addVoteRow}
</table>
`;
}
renderQuestionnaireSkeleton() {
return nothing;
}
renderQuestionnaire() {
// TODO(jrobbins): Implement questionnaires later.
return nothing;
}
renderCommentsSkeleton() {
return html`
<h2>Comments &amp; Activity</h2>
<sl-skeleton effect="sheen"></sl-skeleton>
`;
}
renderComments() {
return html`
<h2>Comments &amp; Activity</h2>
TODO(jrobbins): Comments go here
`;
}
render() {
return html`
${this.loading ?
this.renderHeadingsSkeleton() :
this.renderHeadings()}
<div id="review-status-area">
${this.loading ?
this.renderReviewStatusSkeleton() :
this.renderReviewStatus()}
</div>
<div id="votes-area">
${this.loading ?
this.renderVotesSkeleton() :
this.renderVotes()}
</div>
${this.loading ?
this.renderQuestionnaireSkeleton() :
this.renderQuestionnaire()}
${this.loading ?
this.renderCommentsSkeleton() :
this.renderComments()}
`;
}
}
customElements.define('chromedash-gate-column', ChromedashGateColumn);

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

@ -32,3 +32,14 @@ export function slotAssignedElements(component, slotName) {
export function clamp(val, lowerBound, upperBound) {
return Math.max(lowerBound, Math.min(upperBound, val));
}
/* Given a feature entry stage entity, look up the related process stage. */
export function findProcessStage(feStage, process) {
for (const processStage of process.stages) {
if (feStage.stage_type == processStage.stage_type) {
return processStage;
}
}
return null;
}

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

@ -72,6 +72,12 @@
--leftnav-selected-color: var(--md-blue-900);
--leftnav-divider-border: 1px solid var(--md-gray-100-alpha);
--sidebar-space: 410px;
--sidebar-width: 400px;
--sidebar-bg: white;
--sidebar-border: 2px solid hsl(0, 0%, 85%);
--sidebar-radius: var(--large-border-radius);
--button-background: inherit;
--button-color: var(--md-gray-900-alpha);
--button-font-size: 10pt;