Skip to content

Commit 45fd318

Browse files
committed
Add costs plugin and fix overwhelming err message size
1 parent 3aaae0d commit 45fd318

14 files changed

Lines changed: 263 additions & 80 deletions

File tree

SingularityService/src/main/java/com/hubspot/singularity/config/UIConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ public static RootUrlMode parse(String value) {
117117
@JsonProperty
118118
private Optional<String> showRequestButtonsForGroup = Optional.empty();
119119

120+
@JsonProperty
121+
private Optional<String> costsApiUrlFormat = Optional.empty();
122+
120123
public boolean isHideNewDeployButton() {
121124
return hideNewDeployButton;
122125
}
@@ -340,4 +343,12 @@ public Optional<String> getShowRequestButtonsForGroup() {
340343
public void setShowRequestButtonsForGroup(Optional<String> showRequestButtonsForGroup) {
341344
this.showRequestButtonsForGroup = showRequestButtonsForGroup;
342345
}
346+
347+
public Optional<String> getCostsApiUrlFormat() {
348+
return costsApiUrlFormat;
349+
}
350+
351+
public void setCostsApiUrlFormat(Optional<String> costsApiUrlFormat) {
352+
this.costsApiUrlFormat = costsApiUrlFormat;
353+
}
343354
}

SingularityService/src/main/java/com/hubspot/singularity/views/IndexView.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public class IndexView extends View {
7373
private final String appJsPath;
7474
private final String appCssPath;
7575
private final String vendorJsPath;
76+
private final String costsApiUrlFormat;
7677

7778
public IndexView(
7879
String singularityUriBase,
@@ -190,6 +191,7 @@ public IndexView(
190191
} catch (IOException ioe) {
191192
throw new RuntimeException(ioe);
192193
}
194+
this.costsApiUrlFormat = uiConfiguration.getCostsApiUrlFormat().orElse("");
193195
}
194196

195197
public String getAppRoot() {
@@ -356,6 +358,10 @@ public String getShowRequestButtonsForGroup() {
356358
return showRequestButtonsForGroup;
357359
}
358360

361+
public String getCostsApiUrlFormat() {
362+
return costsApiUrlFormat;
363+
}
364+
359365
@Override
360366
public String toString() {
361367
return (
@@ -463,6 +469,9 @@ public String toString() {
463469
", vendorJsPath='" +
464470
vendorJsPath +
465471
'\'' +
472+
", costsApiUrlFormat='" +
473+
costsApiUrlFormat +
474+
'\'' +
466475
"} " +
467476
super.toString()
468477
);

SingularityUI/app/actions/api/base.es6

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function buildApiAction(actionName, opts = {}, keyFunc = undefined) {
4040
window.location.href = config.redirectOnUnauthorizedUrl.replace('{URL}', encodeURIComponent(window.location.href));
4141
} else { // Something else happened, display the error
4242
Messenger().post({
43-
message: `<p>An error occurred while accessing <code>${options.url}</code></p><pre>${err}</pre>`,
43+
message: `<p>An error occurred while accessing <code>${options.url}</code></p><div class='err-message-content'><pre>${err}</pre></div>`,
4444
type: 'error'
4545
});
4646
}
@@ -82,13 +82,16 @@ export function buildApiAction(actionName, opts = {}, keyFunc = undefined) {
8282
options.headers.Authorization = Utils.getAuthTokenHeader();
8383
}
8484

85-
return fetch(config.apiRoot + options.url + userParam, _.extend({credentials: 'include'}, _.omit(options, 'url')))
85+
const baseUrl = options.url.startsWith('https://') ? options.url : config.apiRoot + options.url;
86+
87+
return fetch(baseUrl + userParam, _.extend({credentials: 'include'}, _.omit(options, 'url')))
8688
.then(response => {
8789
apiResponse = response;
8890
if (response.status === 204) {
8991
return Promise.resolve();
9092
}
91-
if (response.headers.get('Content-Type') === 'application/json') {
93+
const contentType = response.headers.get('Content-Type');
94+
if (contentType === 'application/json' || contentType === 'application/json;charset=utf-8') {
9295
// void response cannot be parsed as JSON
9396
if (response.headers.get('Content-Length') === '0') {
9497
return Promise.resolve();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { buildApiAction } from './base';
2+
3+
export const FetchCostData = buildApiAction(
4+
'FETCH_COSTS',
5+
(requestId, costsUrlFormat) => {
6+
const url = costsUrlFormat.replace('{REQUEST_ID}', requestId);
7+
return ({
8+
url: url,
9+
catchStatusCodes: [404]
10+
})
11+
},
12+
(requestId) => requestId
13+
);

SingularityUI/app/assets/index.mustache

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
quickLinks: {{{quickLinks}}},
5656
navTitleLinks: {{{navTitleLinks}}},
5757
lessTerminalPath: "{{{lessTerminalPath}}}",
58-
showRequestButtonsForGroup: "{{{showRequestButtonsForGroup}}}"
58+
showRequestButtonsForGroup: "{{{showRequestButtonsForGroup}}}",
59+
costsApiUrlFormat: "{{{costsApiUrlFormat}}}"
60+
5961
};
6062
</script>
6163
<script src="{{{staticRoot}}}/{{{vendorJsPath}}}"></script>

SingularityUI/app/components/common/table/UITable.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class UITable extends Component {
187187
);
188188
});
189189

190-
if (sortDirection === UITable.SortDirection.ASC) {
190+
if (sortDirection === UITable.SortDirection.DESC) {
191191
sorted.reverse();
192192
}
193193

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { PropTypes } from 'react';
2+
import { connect } from 'react-redux';
3+
4+
import { Col } from 'react-bootstrap';
5+
6+
import CollapsableSection from '../common/CollapsableSection';
7+
import UITable from '../common/table/UITable';
8+
import Column from '../common/table/Column';
9+
import Utils from '../../utils';
10+
11+
const CostsView = ({requestId, costsAPI}) => {
12+
const costs = costsAPI ? costsAPI.data : [];
13+
const title = costs.length ? 'Average Daily Costs ($' + costs.map((c) => c.cost).reduce((p, c) => p + c).toFixed(4) + ')' : 'Average Daily Costs';
14+
return (
15+
<CollapsableSection id="costs" title={title} defaultExpanded={true}>
16+
<UITable
17+
data={costs}
18+
keyGetter={(c) => c.activityType + c.cost + c.costKey+ c.costType}
19+
defaultSortBy={'cost'}
20+
defaultSortDirection={'DESC'}
21+
showPageLoaderWhenFetching={true}
22+
isFetching={!costs.length}
23+
>
24+
<Column
25+
label="Activity Type"
26+
id="activityType"
27+
key="activityType"
28+
cellData={(c) => Utils.humanizeText(c.activityType)}
29+
/>
30+
<Column
31+
label="Cost Primary Key"
32+
id="costKey"
33+
key="costKey"
34+
cellData={(c) => c.primaryKey}
35+
/>
36+
<Column
37+
label="Cost Type"
38+
id="costType"
39+
key="costType"
40+
cellData={(c) => Utils.humanizeText(c.costType)}
41+
/>
42+
<Column
43+
label="Cost"
44+
id="cost"
45+
key="cost"
46+
forceSortHeader={true}
47+
cellData={(c) => c.cost}
48+
cellRender={(c) => '$' + c}
49+
/>
50+
</UITable>
51+
</CollapsableSection>
52+
);
53+
}
54+
55+
CostsView.propTypes = {
56+
requestId: PropTypes.string.isRequired,
57+
costsAPI: PropTypes.object
58+
};
59+
60+
const mapStateToProps = (state, ownProps) => ({
61+
costsAPI: Utils.maybe(state.api.costs, [ownProps.requestId])
62+
});
63+
64+
export default connect(
65+
mapStateToProps
66+
)(CostsView);

SingularityUI/app/components/requestDetail/RequestDetailPage.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { withRouter } from 'react-router';
44

55
import rootComponent from '../../rootComponent';
66
import * as RefreshActions from '../../actions/ui/refresh';
7+
import { FetchCostData } from '../../actions/api/costs';
78
import { FetchRequest } from '../../actions/api/requests';
89
import {
910
FetchActiveTasksForRequest,
@@ -16,6 +17,7 @@ import {
1617
FetchTaskCleanups
1718
} from '../../actions/api/tasks';
1819

20+
import CostsView from './CostsView';
1921
import RequestHeader from './RequestHeader';
2022
import RequestExpiringActions from './RequestExpiringActions';
2123
import ActiveTasksTable from './ActiveTasksTable';
@@ -32,6 +34,10 @@ import { refresh, initialize } from '../../actions/ui/requestDetail';
3234
class RequestDetailPage extends Component {
3335
componentDidMount() {
3436
this.props.refresh();
37+
if (config.costsApiUrlFormat) {
38+
const { requestId } = this.props.params;
39+
this.props.fetchCostsData(requestId);
40+
}
3541
}
3642

3743
componentWillReceiveProps(nextProps) {
@@ -63,6 +69,7 @@ class RequestDetailPage extends Component {
6369
initialPageNumber={Number(taskHistoryPage) || 1}
6470
/>
6571
)}
72+
{deleted || <CostsView requestId={requestId}/>}
6673
{deleted || <RequestUtilization requestId={requestId} />}
6774
{deleted || <DeployHistoryTable requestId={requestId} />}
6875
<RequestHistoryTable requestId={requestId} />
@@ -108,6 +115,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
108115
cancelRefresh: () => dispatch(
109116
RefreshActions.CancelAutoRefresh(`RequestDetailPage-${ownProps.index}`)
110117
),
118+
fetchCostsData: (requestId) => dispatch(FetchCostData.trigger(requestId, config.costsApiUrlFormat)),
111119
fetchRequest: (requestId) => dispatch(FetchRequest.trigger(requestId, true)),
112120
fetchTaskCleanups: () => dispatch(FetchTaskCleanups.trigger()),
113121
fetchTaskHistoryForRequest: (requestId, count, page) => dispatch(FetchTaskHistoryForRequest.trigger(requestId, count, page)),

SingularityUI/app/components/requestDetail/header/RequestActionButtons.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@ RequestActionButtons.propTypes = {
241241
fetchRequest: PropTypes.func.isRequired,
242242
fetchActiveTasks: PropTypes.func.isRequired,
243243
router: PropTypes.shape({ push: PropTypes.func.isRequired }).isRequired,
244-
admin: PropTypes.bool.isRequired,
245244
};
246245

247246
const mapStateToProps = (state, ownProps) => ({

SingularityUI/app/reducers/api/index.es6

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ import {
3838
ReactivateRack
3939
} from '../../actions/api/racks';
4040

41+
import {
42+
FetchCostData
43+
} from '../../actions/api/costs';
44+
4145
import {
4246
FetchRequests,
4347
FetchRequestIds,
@@ -116,6 +120,7 @@ const freezeRack = buildApiActionReducer(FreezeRack, []);
116120
const decommissionRack = buildApiActionReducer(DecommissionRack, []);
117121
const removeRack = buildApiActionReducer(RemoveRack, []);
118122
const reactivateRack = buildApiActionReducer(ReactivateRack, []);
123+
const costs = buildKeyedApiActionReducer(FetchCostData, []);
119124
const request = buildKeyedApiActionReducer(FetchRequest);
120125
const requestIds = buildApiActionReducer(FetchRequestIds, [])
121126
const saveRequest = buildApiActionReducer(SaveRequest);
@@ -175,6 +180,7 @@ export default combineReducers({
175180
decommissionRack,
176181
removeRack,
177182
reactivateRack,
183+
costs,
178184
request,
179185
saveRequest,
180186
removeRequest,

0 commit comments

Comments
 (0)