Merge pull request #367 from jenkinsci/feature/JENKINS-35840-35781-more-favoriting
Feature/jenkins 35840 35781 more favoriting
This commit is contained in:
commit
e84f3204e0
|
@ -3,6 +3,7 @@ import { CommitHash, ReadableDate } from '@jenkins-cd/design-language';
|
|||
import { LiveStatusIndicator, WeatherIcon } from '@jenkins-cd/design-language';
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
import RunPipeline from './RunPipeline.jsx';
|
||||
import { StopPropagation } from './StopPropagation';
|
||||
import { buildRunDetailsUrl } from '../util/UrlUtils';
|
||||
|
||||
const { object } = PropTypes;
|
||||
|
@ -43,22 +44,30 @@ export default class Branches extends Component {
|
|||
};
|
||||
const { msg } = changeSet[0] || {};
|
||||
|
||||
return (<tr key={cleanBranchName} onClick={open} id={`${cleanBranchName}-${id}`} >
|
||||
<td><WeatherIcon score={weatherScore} /></td>
|
||||
<td onClick={open}>
|
||||
<LiveStatusIndicator result={result === 'UNKNOWN' ? state : result}
|
||||
startTime={startTime} estimatedDuration={estimatedDurationInMillis}
|
||||
/>
|
||||
</td>
|
||||
<td>{cleanBranchName}</td>
|
||||
<td><CommitHash commitId={commitId} /></td>
|
||||
<td>{msg || '-'}</td>
|
||||
<td><ReadableDate date={endTime} liveUpdate /></td>
|
||||
<td>
|
||||
<RunPipeline organization={organization} pipeline={fullName} branch={encodeURIComponent(branchName)} />
|
||||
<Extensions.Renderer extensionPoint="jenkins.pipeline.branches.list.action" />
|
||||
</td>
|
||||
</tr>);
|
||||
return (
|
||||
<tr key={cleanBranchName} onClick={open} id={`${cleanBranchName}-${id}`} >
|
||||
<td><WeatherIcon score={weatherScore} /></td>
|
||||
<td onClick={open}>
|
||||
<LiveStatusIndicator result={result === 'UNKNOWN' ? state : result}
|
||||
startTime={startTime} estimatedDuration={estimatedDurationInMillis}
|
||||
/>
|
||||
</td>
|
||||
<td>{cleanBranchName}</td>
|
||||
<td><CommitHash commitId={commitId} /></td>
|
||||
<td>{msg || '-'}</td>
|
||||
<td><ReadableDate date={endTime} liveUpdate /></td>
|
||||
<td>
|
||||
<StopPropagation className="actions">
|
||||
<RunPipeline organization={organization} pipeline={fullName} branch={encodeURIComponent(branchName)} />
|
||||
<Extensions.Renderer
|
||||
extensionPoint="jenkins.pipeline.branches.list.action"
|
||||
pipeline={data}
|
||||
store={this.context.store}
|
||||
/>
|
||||
</StopPropagation>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,8 +75,8 @@ Branches.propTypes = {
|
|||
data: object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
Branches.contextTypes = {
|
||||
store: object,
|
||||
pipeline: object,
|
||||
router: object.isRequired, // From react-router
|
||||
location: object,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
import { isFailure, isPending } from '../util/FetchStatus';
|
||||
import NotFound from './NotFound';
|
||||
import {
|
||||
|
@ -9,7 +10,6 @@ import {
|
|||
PageTabs,
|
||||
TabLink,
|
||||
WeatherIcon,
|
||||
Favorite,
|
||||
} from '@jenkins-cd/design-language';
|
||||
import { buildOrganizationUrl, buildPipelineUrl } from '../util/UrlUtils';
|
||||
|
||||
|
@ -27,7 +27,7 @@ export default class PipelinePage extends Component {
|
|||
if (isPending(pipeline)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (isFailure(pipeline)) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
@ -44,7 +44,11 @@ export default class PipelinePage extends Component {
|
|||
<span> / </span>
|
||||
<Link to={activityUrl}>{name}</Link>
|
||||
</h1>
|
||||
<Favorite className="dark-yellow" />
|
||||
<Extensions.Renderer
|
||||
extensionPoint="jenkins.pipeline.detail.header.action"
|
||||
store={this.context.store}
|
||||
pipeline={this.context.pipeline}
|
||||
/>
|
||||
</Title>
|
||||
<PageTabs base={baseUrl}>
|
||||
<TabLink to="/activity">Activity</TabLink>
|
||||
|
@ -65,4 +69,5 @@ PipelinePage.propTypes = {
|
|||
PipelinePage.contextTypes = {
|
||||
location: PropTypes.object,
|
||||
pipeline: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -91,6 +91,12 @@ class RunDetails extends Component {
|
|||
decodeURIComponent(run.pipeline) === branch;
|
||||
})[0];
|
||||
|
||||
// deep-linking across RunDetails for different pipelines yields 'runs' data for the wrong pipeline
|
||||
// during initial render. when runs are refetched the screen will render again with 'currentRun' correctly set
|
||||
if (!currentRun) {
|
||||
return null;
|
||||
}
|
||||
|
||||
currentRun.name = name;
|
||||
|
||||
const status = currentRun.result === 'UNKNOWN' ? currentRun.state : currentRun.result;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/27/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
/**
|
||||
* Stops propagation of click events inside this container.
|
||||
* Useful for areas in UI where children should always handle the event, no matter what parent listeners are bound.
|
||||
*
|
||||
* This is a workaround for the following scenario:
|
||||
* 1. Parent DOM element has a click listener,
|
||||
* 2. Child DOM element added via an extension point calls event.stopPropagation() in its own click listener.
|
||||
*
|
||||
* This fails to work, even when calling stopProp and stopImmediateProp on the native event,
|
||||
* probably beacuse there are two React trees each with their own document listener.
|
||||
*
|
||||
* see: http://stackoverflow.com/questions/24415631/reactjs-syntheticevent-stoppropagation-only-works-with-react-events
|
||||
*/
|
||||
export class StopPropagation extends Component {
|
||||
|
||||
_suppressEvent(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={this.props.className}
|
||||
onClick={(event) => this._suppressEvent(event)}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StopPropagation.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
|
@ -72,6 +72,8 @@ export const PullRequestRecord = Record({
|
|||
});
|
||||
|
||||
export const RunsRecord = Record({
|
||||
_class: null,
|
||||
_links: null,
|
||||
latestRun: ActivityRecord,
|
||||
name: null,
|
||||
weatherScore: 0,
|
||||
|
|
|
@ -48,115 +48,121 @@
|
|||
}
|
||||
|
||||
.pipelines-table {
|
||||
th {
|
||||
width: 10%;
|
||||
min-width: 100px;
|
||||
}
|
||||
th {
|
||||
width: 10%;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: auto;
|
||||
}
|
||||
.name {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
width: 30px;
|
||||
}
|
||||
.favorite {
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-table {
|
||||
th {
|
||||
width: 75px;
|
||||
}
|
||||
th {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.branch {
|
||||
width: 175px;
|
||||
}
|
||||
.branch {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 50%;
|
||||
}
|
||||
.message {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.duration, .completed {
|
||||
width: 125px;
|
||||
}
|
||||
.duration, .completed {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
.status-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.multibranch-table {
|
||||
th {
|
||||
width: 75px;
|
||||
}
|
||||
th {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.branch {
|
||||
width: 200px;
|
||||
}
|
||||
.branch {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 50%;
|
||||
}
|
||||
.message {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.lastcommit, .completed {
|
||||
width: 125px;
|
||||
}
|
||||
.lastcommit, .completed {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pr-table {
|
||||
th {
|
||||
width: 75px;
|
||||
}
|
||||
th {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
width: 100%;
|
||||
}
|
||||
.summary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.build, .completed {
|
||||
width: 125px;
|
||||
}
|
||||
.build, .completed {
|
||||
width: 125px;
|
||||
}
|
||||
}
|
||||
|
||||
.changeset-table {
|
||||
th {
|
||||
width: 100px;
|
||||
}
|
||||
th {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.author {
|
||||
width: 150px;
|
||||
}
|
||||
.author {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 100%;
|
||||
}
|
||||
.message {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date {
|
||||
width: 125px;
|
||||
}
|
||||
.date {
|
||||
width: 125px;
|
||||
}
|
||||
}
|
||||
|
||||
.artifacts-table {
|
||||
th {
|
||||
width: 100px;
|
||||
}
|
||||
th {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
}
|
||||
.name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.download {
|
||||
text-align: right;
|
||||
}
|
||||
.download {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.nodes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nodes__section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logConsole .result-item-children {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "0.0.63",
|
||||
"@jenkins-cd/js-extensions": "0.0.17-beta-1",
|
||||
"@jenkins-cd/js-extensions": "0.0.19",
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"@jenkins-cd/sse-gateway": "0.0.5",
|
||||
"immutable": "3.8.1",
|
||||
|
|
|
@ -6,9 +6,10 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import { List } from 'immutable';
|
||||
|
||||
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { actions } from '../redux/FavoritesActions';
|
||||
|
||||
import FavoritesProvider from './FavoritesProvider';
|
||||
import { PipelineCard } from './PipelineCard';
|
||||
|
||||
// the order the cards should be displayed based on their result/state (aka 'status')
|
||||
|
@ -90,54 +91,11 @@ const extractPath = (path, begin, end) => {
|
|||
*/
|
||||
export class DashboardCards extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.fetchUserInProgress = false;
|
||||
this.fetchFavoritesInProgress = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._initialize(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this._initialize(props);
|
||||
}
|
||||
|
||||
_initialize(props) {
|
||||
const config = this.context.config;
|
||||
const { user, favorites } = props;
|
||||
|
||||
if (user) {
|
||||
this.fetchUserInProgress = false;
|
||||
}
|
||||
|
||||
if (favorites) {
|
||||
this.fetchFavoritesInProgress = false;
|
||||
}
|
||||
|
||||
if (config) {
|
||||
const shouldFetchUser = !user && !this.fetchUserInProgress;
|
||||
const shouldFetchFavorites = user && !favorites && !this.fetchFavoritesInProgress;
|
||||
|
||||
if (shouldFetchUser) {
|
||||
this.fetchUserInProgress = true;
|
||||
this.props.fetchUser(config);
|
||||
}
|
||||
|
||||
if (shouldFetchFavorites) {
|
||||
this.fetchFavoritesInProgress = true;
|
||||
this.props.fetchFavorites(config, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onFavoriteToggle(isFavorite, favorite) {
|
||||
this.props.toggleFavorite(isFavorite, favorite.item);
|
||||
this.props.toggleFavorite(isFavorite, favorite.item, favorite);
|
||||
}
|
||||
|
||||
render() {
|
||||
_renderCardStack() {
|
||||
if (!this.props.favorites) {
|
||||
return null;
|
||||
}
|
||||
|
@ -168,6 +126,7 @@ export class DashboardCards extends Component {
|
|||
let startTime = null;
|
||||
let estimatedDuration = null;
|
||||
let commitId = null;
|
||||
let runId = null;
|
||||
|
||||
if (latestRun) {
|
||||
if (latestRun.result) {
|
||||
|
@ -177,6 +136,7 @@ export class DashboardCards extends Component {
|
|||
startTime = latestRun.startTime;
|
||||
estimatedDuration = latestRun.estimatedDurationInMillis;
|
||||
commitId = latestRun.commitId;
|
||||
runId = latestRun.id;
|
||||
}
|
||||
|
||||
if (latestRun && latestRun.result) {
|
||||
|
@ -194,6 +154,7 @@ export class DashboardCards extends Component {
|
|||
pipeline={pipelineName}
|
||||
branch={branchName}
|
||||
commitId={commitId}
|
||||
runId={runId}
|
||||
favorite
|
||||
onFavoriteToggle={(isFavorite) => this._onFavoriteToggle(isFavorite, favorite)}
|
||||
/>
|
||||
|
@ -207,23 +168,25 @@ export class DashboardCards extends Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FavoritesProvider store={this.props.store}>
|
||||
{ this._renderCardStack() }
|
||||
</FavoritesProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DashboardCards.propTypes = {
|
||||
user: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
favorites: PropTypes.instanceOf(List),
|
||||
fetchUser: PropTypes.func,
|
||||
fetchFavorites: PropTypes.func,
|
||||
toggleFavorite: PropTypes.func,
|
||||
};
|
||||
|
||||
DashboardCards.contextTypes = {
|
||||
config: PropTypes.object,
|
||||
};
|
||||
|
||||
const selectors = createSelector(
|
||||
[userSelector, favoritesSelector],
|
||||
(user, favorites) => ({ user, favorites })
|
||||
[favoritesSelector],
|
||||
(favorites) => ({ favorites })
|
||||
);
|
||||
|
||||
export default connect(selectors, actions)(DashboardCards);
|
||||
|
|
|
@ -11,8 +11,12 @@ import { Favorite } from '@jenkins-cd/design-language';
|
|||
import { favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { actions } from '../redux/FavoritesActions';
|
||||
import { checkMatchingFavoriteUrls } from '../util/FavoriteUtils';
|
||||
import FavoritesProvider from './FavoritesProvider';
|
||||
|
||||
/**
|
||||
* A toggle button to favorite or unfavorite the provided item (pipeline or branch)
|
||||
* Contains all logic for rendering the current favorite status of that item
|
||||
* and toggling favorited state on the server.
|
||||
*/
|
||||
export class FavoritePipeline extends Component {
|
||||
|
||||
|
@ -34,18 +38,21 @@ export class FavoritePipeline extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
_findMatchingFavorite(pipeline, favorites) {
|
||||
if (!pipeline || !favorites) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return favorites.find((fav) => {
|
||||
const favUrl = fav.item._links.self.href;
|
||||
const pipelineUrl = pipeline._links.self.href;
|
||||
return checkMatchingFavoriteUrls(favUrl, pipelineUrl);
|
||||
});
|
||||
}
|
||||
|
||||
_updateState(props) {
|
||||
const { pipeline } = props;
|
||||
let favorite = null;
|
||||
|
||||
if (props.favorites) {
|
||||
favorite = props.favorites.find((fav) => {
|
||||
const favUrl = fav.item._links.self.href;
|
||||
const pipelineUrl = pipeline._links.self.href;
|
||||
|
||||
return checkMatchingFavoriteUrls(favUrl, pipelineUrl);
|
||||
});
|
||||
}
|
||||
const favorite = this._findMatchingFavorite(pipeline, props.favorites);
|
||||
|
||||
this.setState({
|
||||
favorite: !!favorite,
|
||||
|
@ -58,16 +65,20 @@ export class FavoritePipeline extends Component {
|
|||
favorite: isFavorite,
|
||||
});
|
||||
|
||||
const favorite = this._findMatchingFavorite(this.props.pipeline, this.props.favorites);
|
||||
|
||||
if (this.props.toggleFavorite) {
|
||||
this.props.toggleFavorite(isFavorite, this.props.pipeline);
|
||||
this.props.toggleFavorite(isFavorite, this.props.pipeline, favorite);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Favorite checked={this.state.favorite} className={this.props.className}
|
||||
onToggle={() => this._onFavoriteToggle()}
|
||||
/>
|
||||
<FavoritesProvider store={this.props.store}>
|
||||
<Favorite checked={this.state.favorite} className={this.props.className}
|
||||
onToggle={() => this._onFavoriteToggle()}
|
||||
/>
|
||||
</FavoritesProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +88,7 @@ FavoritePipeline.propTypes = {
|
|||
pipeline: PropTypes.object,
|
||||
favorites: PropTypes.instanceOf(List),
|
||||
toggleFavorite: PropTypes.func,
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
FavoritePipeline.defaultProps = {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import React, { Component } from 'react';
|
||||
import FavoritePipeline from './FavoritePipeline';
|
||||
|
||||
/**
|
||||
* Restyled version of FavoritePipeline component.
|
||||
*
|
||||
* Created by cmeyers on 7/20/16.
|
||||
*/
|
||||
export class FavoritePipelineHeader extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FavoritePipeline { ...this.props } className="dark-yellow" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FavoritePipelineHeader;
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Created by cmeyers on 7/20/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { List } from 'immutable';
|
||||
|
||||
import { userSelector, favoritesSelector } from '../redux/FavoritesStore';
|
||||
import { actions } from '../redux/FavoritesActions';
|
||||
|
||||
/**
|
||||
* FavoritesProvider ensures that the current user's favorites
|
||||
* are loaded for any components which may need it.
|
||||
*
|
||||
* Components that require this data can simply wrap themselves in
|
||||
* FavoritesProvider which will ensure the store is updated correctly.
|
||||
*/
|
||||
export class FavoritesProvider extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
this._initialize(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this._initialize(props);
|
||||
}
|
||||
|
||||
_initialize(props) {
|
||||
const { user, favorites } = props;
|
||||
|
||||
const shouldFetchUser = !user;
|
||||
const shouldFetchFavorites = user && !favorites;
|
||||
|
||||
if (shouldFetchUser) {
|
||||
this.props.fetchUser();
|
||||
}
|
||||
|
||||
if (shouldFetchFavorites) {
|
||||
this.props.fetchFavorites(user);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children ?
|
||||
React.cloneElement(this.props.children, { ...this.props }) :
|
||||
null;
|
||||
}
|
||||
}
|
||||
|
||||
FavoritesProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
user: PropTypes.object,
|
||||
favorites: PropTypes.instanceOf(List),
|
||||
fetchUser: PropTypes.func,
|
||||
fetchFavorites: PropTypes.func,
|
||||
};
|
||||
|
||||
const selectors = createSelector(
|
||||
[userSelector, favoritesSelector],
|
||||
(user, favorites) => ({ user, favorites })
|
||||
);
|
||||
|
||||
export default connect(selectors, actions)(FavoritesProvider);
|
|
@ -2,6 +2,7 @@
|
|||
* Created by cmeyers on 6/28/16.
|
||||
*/
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Icon } from 'react-material-icons-blue';
|
||||
import { Favorite, LiveStatusIndicator } from '@jenkins-cd/design-language';
|
||||
|
||||
|
@ -75,6 +76,10 @@ export class PipelineCard extends Component {
|
|||
const showRun = status && (status.toLowerCase() === 'failure' || status.toLowerCase() === 'aborted');
|
||||
const commitText = commitId ? commitId.substr(0, 7) : '';
|
||||
|
||||
const runUrl = `/organizations/${encodeURIComponent(this.props.organization)}/` +
|
||||
`${encodeURIComponent(this.props.fullName)}/detail/` +
|
||||
`${encodeURIComponent(this.props.branch || this.props.pipeline)}/${encodeURIComponent(this.props.runId)}/pipeline`;
|
||||
|
||||
return (
|
||||
<div className={`pipeline-card ${bgClass}`}>
|
||||
<LiveStatusIndicator
|
||||
|
@ -83,7 +88,9 @@ export class PipelineCard extends Component {
|
|||
/>
|
||||
|
||||
<span className="name">
|
||||
{this.props.organization} / <span title={this.props.fullName}>{this.props.pipeline}</span>
|
||||
<Link to={runUrl}>
|
||||
{this.props.organization} / <span title={this.props.fullName}>{this.props.pipeline}</span>
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
{ this.props.branch ?
|
||||
|
@ -129,6 +136,7 @@ PipelineCard.propTypes = {
|
|||
pipeline: PropTypes.string,
|
||||
branch: PropTypes.string,
|
||||
commitId: PropTypes.string,
|
||||
runId: PropTypes.string,
|
||||
favorite: PropTypes.bool,
|
||||
onRunClick: PropTypes.func,
|
||||
onFavoriteToggle: PropTypes.func,
|
||||
|
|
|
@ -8,3 +8,7 @@ extensions:
|
|||
extensionPoint: jenkins.pipeline.list.top
|
||||
- component: components/FavoritePipeline
|
||||
extensionPoint: jenkins.pipeline.list.action
|
||||
- component: components/FavoritePipelineHeader
|
||||
extensionPoint: jenkins.pipeline.detail.header.action
|
||||
- component: components/FavoritePipeline
|
||||
extensionPoint: jenkins.pipeline.branches.list.action
|
||||
|
|
|
@ -33,13 +33,24 @@ function parseJSON(response) {
|
|||
});
|
||||
}
|
||||
|
||||
const fetchFlags = {
|
||||
[ACTION_TYPES.SET_USER]: false,
|
||||
[ACTION_TYPES.SET_FAVORITES]: false,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
fetchUser(config) {
|
||||
fetchUser() {
|
||||
return (dispatch) => {
|
||||
const baseUrl = config.getAppURLBase();
|
||||
const baseUrl = urlConfig.blueoceanAppURL;
|
||||
const url = `${baseUrl}/rest/organizations/jenkins/user/`;
|
||||
const fetchOptions = { ...defaultFetchOptions };
|
||||
|
||||
if (fetchFlags[ACTION_TYPES.SET_USER]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
fetchFlags[ACTION_TYPES.SET_USER] = true;
|
||||
|
||||
return dispatch(actions.generateData(
|
||||
{ url, fetchOptions },
|
||||
ACTION_TYPES.SET_USER
|
||||
|
@ -47,13 +58,19 @@ export const actions = {
|
|||
};
|
||||
},
|
||||
|
||||
fetchFavorites(config, user) {
|
||||
fetchFavorites(user) {
|
||||
return (dispatch) => {
|
||||
const baseUrl = config.getAppURLBase();
|
||||
const baseUrl = urlConfig.blueoceanAppURL;
|
||||
const username = user.id;
|
||||
const url = `${baseUrl}/rest/users/${username}/favorites/`;
|
||||
const fetchOptions = { ...defaultFetchOptions };
|
||||
|
||||
if (fetchFlags[ACTION_TYPES.SET_FAVORITES]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
fetchFlags[ACTION_TYPES.SET_FAVORITES] = true;
|
||||
|
||||
return dispatch(actions.generateData(
|
||||
{ url, fetchOptions },
|
||||
ACTION_TYPES.SET_FAVORITES
|
||||
|
@ -61,10 +78,14 @@ export const actions = {
|
|||
};
|
||||
},
|
||||
|
||||
toggleFavorite(addFavorite, branch) {
|
||||
toggleFavorite(addFavorite, branch, favoriteToRemove) {
|
||||
return (dispatch) => {
|
||||
const baseUrl = urlConfig.jenkinsRootURL;
|
||||
const url = `${baseUrl}${branch._links.self.href}/favorite`;
|
||||
|
||||
const url = addFavorite ?
|
||||
`${baseUrl}${branch._links.self.href}/favorite` :
|
||||
`${baseUrl}${favoriteToRemove._links.self.href}`;
|
||||
|
||||
const fetchOptions = {
|
||||
...defaultFetchOptions,
|
||||
method: 'PUT',
|
||||
|
@ -89,12 +110,16 @@ export const actions = {
|
|||
return (dispatch) => fetch(url, fetchOptions)
|
||||
.then(checkStatus)
|
||||
.then(parseJSON)
|
||||
.then(json => dispatch({
|
||||
...optional,
|
||||
type: actionType,
|
||||
payload: json,
|
||||
}))
|
||||
.then((json) => {
|
||||
fetchFlags[actionType] = false;
|
||||
return dispatch({
|
||||
...optional,
|
||||
type: actionType,
|
||||
payload: json,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFlags[actionType] = false;
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
// call again with no payload so actions handle missing data
|
||||
dispatch({
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
min-width: 400px;
|
||||
padding: 15px;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.name, .branch, .commit {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
@import 'components/pipeline-card';
|
||||
|
||||
.multibranch-table .actions .checkbox {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.favorites-card-stack {
|
||||
margin-bottom: 40px;
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('PipelineCard', () => {
|
|||
|
||||
assert.equal(wrapper.find('LiveStatusIndicator').length, 1);
|
||||
assert.equal(wrapper.find('.name').length, 1);
|
||||
assert.equal(wrapper.find('.name').text(), 'Jenkins / blueocean');
|
||||
assert.equal(wrapper.find('.name').text(), '<Link />');
|
||||
assert.equal(wrapper.find('.branch').length, 1);
|
||||
assert.equal(wrapper.find('.branchText').text(), 'feature/JENKINS-123');
|
||||
assert.equal(wrapper.find('.commit').length, 1);
|
||||
|
|
|
@ -54,13 +54,17 @@ public class JenkinsJSExtensionsTest extends BaseTest {
|
|||
Assert.assertEquals("AdminNavLink", extensionPoints.get(0).get("component"));
|
||||
Assert.assertEquals("jenkins.logo.top", extensionPoints.get(0).get("extensionPoint"));
|
||||
} else if ("blueocean-personalization".equals(pluginId)) {
|
||||
Assert.assertEquals(3, extensionPoints.size());
|
||||
Assert.assertEquals(5, extensionPoints.size());
|
||||
Assert.assertEquals("redux/FavoritesStore", extensionPoints.get(0).get("component"));
|
||||
Assert.assertEquals("jenkins.main.stores", extensionPoints.get(0).get("extensionPoint"));
|
||||
Assert.assertEquals("components/DashboardCards", extensionPoints.get(1).get("component"));
|
||||
Assert.assertEquals("jenkins.pipeline.list.top", extensionPoints.get(1).get("extensionPoint"));
|
||||
Assert.assertEquals("components/FavoritePipeline", extensionPoints.get(2).get("component"));
|
||||
Assert.assertEquals("jenkins.pipeline.list.action", extensionPoints.get(2).get("extensionPoint"));
|
||||
Assert.assertEquals("components/FavoritePipelineHeader", extensionPoints.get(3).get("component"));
|
||||
Assert.assertEquals("jenkins.pipeline.detail.header.action", extensionPoints.get(3).get("extensionPoint"));
|
||||
Assert.assertEquals("components/FavoritePipeline", extensionPoints.get(4).get("component"));
|
||||
Assert.assertEquals("jenkins.pipeline.branches.list.action", extensionPoints.get(4).get("extensionPoint"));
|
||||
} else {
|
||||
Assert.fail("Found extensions from unknown pluginId: " + pluginId);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue