Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public static RootUrlMode parse(String value) {
@JsonProperty
private Optional<String> navColor = Optional.empty();

@JsonProperty
private List<UINavLinkConfiguration> formattedNavLinks = Collections.emptyList();

@JsonProperty
private String baseUrl;

Expand Down Expand Up @@ -109,6 +112,7 @@ public static RootUrlMode parse(String value) {

// e.g. {"QA": "https://singularity-qa.my-paas.net", "Production": "https://singularity-prod.my-paas.net"}
@JsonProperty
@Deprecated
private Map<String, String> navTitleLinks = Collections.emptyMap();

@JsonProperty
Expand All @@ -117,6 +121,9 @@ public static RootUrlMode parse(String value) {
@JsonProperty
private Optional<String> showRequestButtonsForGroup = Optional.empty();

@JsonProperty
private Optional<String> costsApiUrlFormat = Optional.empty();

public boolean isHideNewDeployButton() {
return hideNewDeployButton;
}
Expand Down Expand Up @@ -340,4 +347,20 @@ public Optional<String> getShowRequestButtonsForGroup() {
public void setShowRequestButtonsForGroup(Optional<String> showRequestButtonsForGroup) {
this.showRequestButtonsForGroup = showRequestButtonsForGroup;
}

public Optional<String> getCostsApiUrlFormat() {
return costsApiUrlFormat;
}

public void setCostsApiUrlFormat(Optional<String> costsApiUrlFormat) {
this.costsApiUrlFormat = costsApiUrlFormat;
}

public List<UINavLinkConfiguration> getFormattedNavLinks() {
return formattedNavLinks;
}

public void setFormattedNavLinks(List<UINavLinkConfiguration> formattedNavLinks) {
this.formattedNavLinks = formattedNavLinks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.hubspot.singularity.config;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.annotation.Nullable;

@JsonIgnoreProperties(ignoreUnknown = true)
public class UINavLinkConfiguration {
private String title;
private String linkFormat;
private Boolean divider = false;
private String tooltip;

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getLinkFormat() {
return linkFormat;
}

public void setLinkFormat(String linkFormat) {
this.linkFormat = linkFormat;
}

public Boolean getDivider() {
return divider;
}

public void setDivider(Boolean divider) {
this.divider = divider;
}

@Nullable
public String getTooltip() {
return tooltip;
}

public void setTooltip(@Nullable String tooltip) {
this.tooltip = tooltip;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public class IndexView extends View {
private final String appJsPath;
private final String appCssPath;
private final String vendorJsPath;
private final String costsApiUrlFormat;

public IndexView(
String singularityUriBase,
Expand Down Expand Up @@ -170,7 +171,7 @@ public IndexView(
}

try {
this.navTitleLinks = ow.writeValueAsString(uiConfiguration.getNavTitleLinks());
this.navTitleLinks = ow.writeValueAsString(uiConfiguration.getFormattedNavLinks());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
Expand All @@ -190,6 +191,7 @@ public IndexView(
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
this.costsApiUrlFormat = uiConfiguration.getCostsApiUrlFormat().orElse("");
}

public String getAppRoot() {
Expand Down Expand Up @@ -356,6 +358,10 @@ public String getShowRequestButtonsForGroup() {
return showRequestButtonsForGroup;
}

public String getCostsApiUrlFormat() {
return costsApiUrlFormat;
}

@Override
public String toString() {
return (
Expand Down Expand Up @@ -463,6 +469,9 @@ public String toString() {
", vendorJsPath='" +
vendorJsPath +
'\'' +
", costsApiUrlFormat='" +
costsApiUrlFormat +
'\'' +
"} " +
super.toString()
);
Expand Down
9 changes: 6 additions & 3 deletions SingularityUI/app/actions/api/base.es6
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function buildApiAction(actionName, opts = {}, keyFunc = undefined) {
window.location.href = config.redirectOnUnauthorizedUrl.replace('{URL}', encodeURIComponent(window.location.href));
} else { // Something else happened, display the error
Messenger().post({
message: `<p>An error occurred while accessing <code>${options.url}</code></p><pre>${err}</pre>`,
message: `<p>An error occurred while accessing <code>${options.url}</code></p><div class='err-message-content'><pre>${err}</pre></div>`,
type: 'error'
});
}
Expand Down Expand Up @@ -82,13 +82,16 @@ export function buildApiAction(actionName, opts = {}, keyFunc = undefined) {
options.headers.Authorization = Utils.getAuthTokenHeader();
}

return fetch(config.apiRoot + options.url + userParam, _.extend({credentials: 'include'}, _.omit(options, 'url')))
const baseUrl = options.url.startsWith('https://') ? options.url : config.apiRoot + options.url;

return fetch(baseUrl + userParam, _.extend({credentials: 'include'}, _.omit(options, 'url')))
.then(response => {
apiResponse = response;
if (response.status === 204) {
return Promise.resolve();
}
if (response.headers.get('Content-Type') === 'application/json') {
const contentType = response.headers.get('Content-Type');
if (contentType === 'application/json' || contentType === 'application/json;charset=utf-8') {
// void response cannot be parsed as JSON
if (response.headers.get('Content-Length') === '0') {
return Promise.resolve();
Expand Down
13 changes: 13 additions & 0 deletions SingularityUI/app/actions/api/costs.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { buildApiAction } from './base';

export const FetchCostData = buildApiAction(
'FETCH_COSTS',
(requestId, costsUrlFormat) => {
const url = costsUrlFormat.replace('{REQUEST_ID}', requestId);
return ({
url: url,
catchStatusCodes: [404]
})
},
(requestId) => requestId
);
4 changes: 3 additions & 1 deletion SingularityUI/app/assets/index.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
quickLinks: {{{quickLinks}}},
navTitleLinks: {{{navTitleLinks}}},
lessTerminalPath: "{{{lessTerminalPath}}}",
showRequestButtonsForGroup: "{{{showRequestButtonsForGroup}}}"
showRequestButtonsForGroup: "{{{showRequestButtonsForGroup}}}",
costsApiUrlFormat: "{{{costsApiUrlFormat}}}"

};
</script>
<script src="{{{staticRoot}}}/{{{vendorJsPath}}}"></script>
Expand Down
30 changes: 25 additions & 5 deletions SingularityUI/app/components/common/Navigation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import classnames from 'classnames';
import Utils from '../../utils';

import { Glyphicon } from 'react-bootstrap';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import ToolTip from 'react-bootstrap/lib/Tooltip';

function handleSearchClick(event, toggleGlobalSearch) {
event.preventDefault();
Expand Down Expand Up @@ -58,11 +60,29 @@ const Navigation = (props) => {
{config.title} <span className="caret" />
</a>
<ul className="dropdown-menu">
{Object.keys(config.navTitleLinks).map((linkTitle, index) =>
<li key={index}>
<a href={config.navTitleLinks[linkTitle].replace('{CURRENT_PATH}', currentPathForLink(props.location.pathname))}>{linkTitle}</a>
</li>
)}
{config.navTitleLinks.map((linkConfig, index) => {
if (linkConfig['divider']) {
return (<li key={index} role="separator" className="divider"></li>);
}
let link = (<a href={linkConfig['linkFormat'].replace('{CURRENT_PATH}', currentPathForLink(props.location.pathname))}>{linkConfig['title']}</a>);
if ('tooltip' in linkConfig) {
const tooltip = (
<ToolTip id="view-nav-tip">
{linkConfig['tooltip']}
</ToolTip>
);
link = (
<OverlayTrigger placement="right" id="view-nav-tip-overlay" overlay={tooltip}>
{link}
</OverlayTrigger>
);
}
return (
<li key={index}>
{link}
</li>
);
})}
</ul>
</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion SingularityUI/app/components/common/table/UITable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class UITable extends Component {
);
});

if (sortDirection === UITable.SortDirection.ASC) {
if (sortDirection === UITable.SortDirection.DESC) {
sorted.reverse();
}

Expand Down
66 changes: 66 additions & 0 deletions SingularityUI/app/components/requestDetail/CostsView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';

import { Col } from 'react-bootstrap';

import CollapsableSection from '../common/CollapsableSection';
import UITable from '../common/table/UITable';
import Column from '../common/table/Column';
import Utils from '../../utils';

const CostsView = ({requestId, costsAPI}) => {
const costs = costsAPI ? costsAPI.data : [];
const title = costs.length ? 'Average Daily Costs ($' + costs.map((c) => c.cost).reduce((p, c) => p + c).toFixed(4) + ')' : 'Average Daily Costs';
return (
<CollapsableSection id="costs" title={title} defaultExpanded={true}>
<UITable
data={costs}
keyGetter={(c) => c.activityType + c.cost + c.costKey+ c.costType}
defaultSortBy={'cost'}
defaultSortDirection={'DESC'}
showPageLoaderWhenFetching={true}
isFetching={!costs.length}
>
<Column
label="Activity Type"
id="activityType"
key="activityType"
cellData={(c) => Utils.humanizeText(c.activityType)}
/>
<Column
label="Cost Primary Key"
id="costKey"
key="costKey"
cellData={(c) => c.primaryKey}
/>
<Column
label="Cost Type"
id="costType"
key="costType"
cellData={(c) => Utils.humanizeText(c.costType)}
/>
<Column
label="Cost"
id="cost"
key="cost"
forceSortHeader={true}
cellData={(c) => c.cost}
cellRender={(c) => '$' + c}
/>
</UITable>
</CollapsableSection>
);
}

CostsView.propTypes = {
requestId: PropTypes.string.isRequired,
costsAPI: PropTypes.object
};

const mapStateToProps = (state, ownProps) => ({
costsAPI: Utils.maybe(state.api.costs, [ownProps.requestId])
});

export default connect(
mapStateToProps
)(CostsView);
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { withRouter } from 'react-router';

import rootComponent from '../../rootComponent';
import * as RefreshActions from '../../actions/ui/refresh';
import { FetchCostData } from '../../actions/api/costs';
import { FetchRequest } from '../../actions/api/requests';
import {
FetchActiveTasksForRequest,
Expand All @@ -16,6 +17,7 @@ import {
FetchTaskCleanups
} from '../../actions/api/tasks';

import CostsView from './CostsView';
import RequestHeader from './RequestHeader';
import RequestExpiringActions from './RequestExpiringActions';
import ActiveTasksTable from './ActiveTasksTable';
Expand All @@ -32,6 +34,10 @@ import { refresh, initialize } from '../../actions/ui/requestDetail';
class RequestDetailPage extends Component {
componentDidMount() {
this.props.refresh();
if (config.costsApiUrlFormat) {
const { requestId } = this.props.params;
this.props.fetchCostsData(requestId);
}
}

componentWillReceiveProps(nextProps) {
Expand Down Expand Up @@ -63,6 +69,7 @@ class RequestDetailPage extends Component {
initialPageNumber={Number(taskHistoryPage) || 1}
/>
)}
{deleted || <CostsView requestId={requestId}/>}
{deleted || <RequestUtilization requestId={requestId} />}
{deleted || <DeployHistoryTable requestId={requestId} />}
<RequestHistoryTable requestId={requestId} />
Expand Down Expand Up @@ -108,6 +115,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
cancelRefresh: () => dispatch(
RefreshActions.CancelAutoRefresh(`RequestDetailPage-${ownProps.index}`)
),
fetchCostsData: (requestId) => dispatch(FetchCostData.trigger(requestId, config.costsApiUrlFormat)),
fetchRequest: (requestId) => dispatch(FetchRequest.trigger(requestId, true)),
fetchTaskCleanups: () => dispatch(FetchTaskCleanups.trigger()),
fetchTaskHistoryForRequest: (requestId, count, page) => dispatch(FetchTaskHistoryForRequest.trigger(requestId, count, page)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,6 @@ RequestActionButtons.propTypes = {
fetchRequest: PropTypes.func.isRequired,
fetchActiveTasks: PropTypes.func.isRequired,
router: PropTypes.shape({ push: PropTypes.func.isRequired }).isRequired,
admin: PropTypes.bool.isRequired,
};

const mapStateToProps = (state, ownProps) => ({
Expand Down
6 changes: 6 additions & 0 deletions SingularityUI/app/reducers/api/index.es6
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import {
ReactivateRack
} from '../../actions/api/racks';

import {
FetchCostData
} from '../../actions/api/costs';

import {
FetchRequests,
FetchRequestIds,
Expand Down Expand Up @@ -116,6 +120,7 @@ const freezeRack = buildApiActionReducer(FreezeRack, []);
const decommissionRack = buildApiActionReducer(DecommissionRack, []);
const removeRack = buildApiActionReducer(RemoveRack, []);
const reactivateRack = buildApiActionReducer(ReactivateRack, []);
const costs = buildKeyedApiActionReducer(FetchCostData, []);
const request = buildKeyedApiActionReducer(FetchRequest);
const requestIds = buildApiActionReducer(FetchRequestIds, [])
const saveRequest = buildApiActionReducer(SaveRequest);
Expand Down Expand Up @@ -175,6 +180,7 @@ export default combineReducers({
decommissionRack,
removeRack,
reactivateRack,
costs,
request,
saveRequest,
removeRequest,
Expand Down
Loading