-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2902 from dlabrecq/3477-ros-api
Boiler plate for ROS APIs
- Loading branch information
Showing
12 changed files
with
399 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { RosType } from 'api/ros/ros'; | ||
import axios from 'axios'; | ||
|
||
import { runRos } from './recommendations'; | ||
|
||
test('api run reports calls axios get', () => { | ||
const query = 'filter[resolution]=daily'; | ||
runRos(RosType.cost, query); | ||
expect(axios.get).toBeCalledWith(`reports/openshift/costs/?${query}`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import axios from 'axios'; | ||
|
||
import type { Ros, RosData, RosItem, RosItemValue, RosMeta, RosValue } from './ros'; | ||
import { RosType } from './ros'; | ||
|
||
export interface RosRosItem extends RosItem { | ||
capacity?: RosValue; | ||
cluster?: string; | ||
clusters?: string[]; | ||
limit?: RosValue; | ||
node?: string; | ||
project?: string; | ||
request?: RosValue; | ||
} | ||
|
||
export interface RosRosData extends RosData { | ||
// TBD... | ||
} | ||
|
||
export interface RosRosMeta extends RosMeta { | ||
total?: { | ||
capacity?: RosValue; | ||
cost?: RosItemValue; | ||
infrastructure?: RosItemValue; | ||
limit?: RosValue; | ||
request?: RosValue; | ||
supplementary?: RosItemValue; | ||
usage?: RosValue; | ||
}; | ||
} | ||
|
||
export interface RosRos extends Ros { | ||
meta: RosRosMeta; | ||
data: RosRosData[]; | ||
} | ||
|
||
export const RosTypePaths: Partial<Record<RosType, string>> = { | ||
[RosType.cost]: 'reports/openshift/costs/', | ||
}; | ||
|
||
export function runRos(reportType: RosType, query: string) { | ||
const path = RosTypePaths[reportType]; | ||
return axios.get<RosRos>(`${path}?${query}`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import type { PagedMetaData, PagedResponse } from 'api/api'; | ||
|
||
export interface RosValue { | ||
units?: string; | ||
value?: number; | ||
} | ||
|
||
export interface RosItemValue { | ||
markup?: RosValue; | ||
raw?: RosValue; | ||
total?: RosValue; | ||
usage: RosValue; | ||
} | ||
|
||
export interface RosItem { | ||
alias?: string; | ||
classification?: string; // Platform. | ||
cost?: RosItemValue; | ||
date?: string; | ||
default_project?: string; // 'True' or 'False' | ||
delta_percent?: number; | ||
delta_value?: number; | ||
id?: string; | ||
infrastructure?: RosItemValue; | ||
source_uuid?: string; | ||
supplementary?: RosItemValue; | ||
} | ||
|
||
// Additional props for group_by[org_unit_id] | ||
export interface RosData { | ||
date?: string; | ||
values?: RosItem[]; | ||
} | ||
|
||
export interface RosMeta extends PagedMetaData { | ||
category?: string[]; | ||
count: number; | ||
delta?: { | ||
percent: number; | ||
value: number; | ||
}; | ||
filter?: { | ||
[filter: string]: any; | ||
}; | ||
group_by?: { | ||
[group: string]: string[]; | ||
}; | ||
order_by?: { | ||
[order: string]: string; | ||
}; | ||
others?: number; | ||
total?: { | ||
capacity?: RosValue; | ||
cost?: RosItemValue; | ||
count?: RosValue; // Workaround for https://github.com/project-koku/koku/issues/1395 | ||
infrastructure?: RosItemValue; | ||
limit?: RosValue; | ||
request?: RosValue; | ||
supplementary?: RosItemValue; | ||
usage?: RosValue; | ||
}; | ||
} | ||
|
||
export interface Ros extends PagedResponse<RosData, RosMeta> {} | ||
|
||
// eslint-disable-next-line no-shadow | ||
export const enum RosType { | ||
cost = 'cost', | ||
} | ||
|
||
// eslint-disable-next-line no-shadow | ||
export const enum RosPathsType { | ||
recommendation = 'recommendation', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { runRos as runRecommendation } from './recommendations'; | ||
import type { RosType } from './ros'; | ||
import { RosPathsType } from './ros'; | ||
|
||
export function runRos(rosPathsType: RosPathsType, rosType: RosType, query: string) { | ||
let result; | ||
switch (rosPathsType) { | ||
case RosPathsType.recommendation: | ||
result = runRecommendation(rosType, query); | ||
break; | ||
} | ||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`default state 1`] = ` | ||
{ | ||
"byId": Map {}, | ||
"errors": Map {}, | ||
"fetchStatus": Map {}, | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import * as rosActions from './rosActions'; | ||
import { rosStateKey } from './rosCommon'; | ||
import type { CachedRos, RosAction, RosState } from './rosReducer'; | ||
import { rosReducer } from './rosReducer'; | ||
import * as rosSelectors from './rosSelectors'; | ||
|
||
export type { RosAction, CachedRos, RosState }; | ||
export { rosActions, rosReducer, rosSelectors, rosStateKey }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
jest.mock('api/ros/rosUtils'); | ||
|
||
import { waitFor } from '@testing-library/react'; | ||
import type { Ros } from 'api/ros/ros'; | ||
import { RosPathsType, RosType } from 'api/ros/ros'; | ||
import { runRos } from 'api/ros/rosUtils'; | ||
import { FetchStatus } from 'store/common'; | ||
import { createMockStoreCreator } from 'store/mockStore'; | ||
|
||
import * as actions from './rosActions'; | ||
import { rosStateKey } from './rosCommon'; | ||
import { rosReducer } from './rosReducer'; | ||
import * as selectors from './rosSelectors'; | ||
|
||
const createRossStore = createMockStoreCreator({ | ||
[rosStateKey]: rosReducer, | ||
}); | ||
|
||
const runRosMock = runRos as jest.Mock; | ||
|
||
const mockRos: Ros = { | ||
data: [], | ||
total: { | ||
value: 100, | ||
units: 'USD', | ||
}, | ||
} as any; | ||
|
||
const rosType = RosType.cost; | ||
const rosPathsType = RosPathsType.recommendation; | ||
const rosQueryString = 'rosQueryString'; | ||
|
||
runRosMock.mockResolvedValue({ data: mockRos }); | ||
global.Date.now = jest.fn(() => 12345); | ||
|
||
jest.spyOn(actions, 'fetchRos'); | ||
jest.spyOn(selectors, 'selectRosFetchStatus'); | ||
|
||
test('default state', () => { | ||
const store = createRossStore(); | ||
expect(selectors.selectRosState(store.getState())).toMatchSnapshot(); | ||
}); | ||
|
||
test('fetch ros success', async () => { | ||
const store = createRossStore(); | ||
store.dispatch(actions.fetchRos(rosPathsType, rosType, rosQueryString)); | ||
expect(runRosMock).toBeCalled(); | ||
expect(selectors.selectRosFetchStatus(store.getState(), rosPathsType, rosType, rosQueryString)).toBe( | ||
FetchStatus.inProgress | ||
); | ||
await waitFor(() => expect(selectors.selectRosFetchStatus).toHaveBeenCalled()); | ||
const finishedState = store.getState(); | ||
expect(selectors.selectRosFetchStatus(finishedState, rosPathsType, rosType, rosQueryString)).toBe( | ||
FetchStatus.complete | ||
); | ||
expect(selectors.selectRosError(finishedState, rosPathsType, rosType, rosQueryString)).toBe(null); | ||
}); | ||
|
||
test('fetch ros failure', async () => { | ||
const store = createRossStore(); | ||
const error = Symbol('ros error'); | ||
runRosMock.mockRejectedValueOnce(error); | ||
store.dispatch(actions.fetchRos(rosPathsType, rosType, rosQueryString)); | ||
expect(runRos).toBeCalled(); | ||
expect(selectors.selectRosFetchStatus(store.getState(), rosPathsType, rosType, rosQueryString)).toBe( | ||
FetchStatus.inProgress | ||
); | ||
await waitFor(() => expect(selectors.selectRosFetchStatus).toHaveBeenCalled()); | ||
const finishedState = store.getState(); | ||
expect(selectors.selectRosFetchStatus(finishedState, rosPathsType, rosType, rosQueryString)).toBe( | ||
FetchStatus.complete | ||
); | ||
expect(selectors.selectRosError(finishedState, rosPathsType, rosType, rosQueryString)).toBe(error); | ||
}); | ||
|
||
test('does not fetch ros if the request is in progress', () => { | ||
const store = createRossStore(); | ||
store.dispatch(actions.fetchRos(rosPathsType, rosType, rosQueryString)); | ||
store.dispatch(actions.fetchRos(rosPathsType, rosType, rosQueryString)); | ||
expect(runRos).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('ros is not refetched if it has not expired', async () => { | ||
const store = createRossStore(); | ||
store.dispatch(actions.fetchRos(rosPathsType, rosType, rosQueryString)); | ||
await waitFor(() => expect(actions.fetchRos).toHaveBeenCalled()); | ||
store.dispatch(actions.fetchRos(rosPathsType, rosType, rosQueryString)); | ||
expect(runRos).toHaveBeenCalledTimes(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import type { Ros } from 'api/ros/ros'; | ||
import type { RosPathsType, RosType } from 'api/ros/ros'; | ||
import { runRos } from 'api/ros/rosUtils'; | ||
import type { AxiosError } from 'axios'; | ||
import type { ThunkAction } from 'store/common'; | ||
import { FetchStatus } from 'store/common'; | ||
import type { RootState } from 'store/rootReducer'; | ||
import { createAction } from 'typesafe-actions'; | ||
|
||
import { getFetchId } from './rosCommon'; | ||
import { selectRos, selectRosFetchStatus } from './rosSelectors'; | ||
|
||
const expirationMS = 30 * 60 * 1000; // 30 minutes | ||
|
||
interface RosActionMeta { | ||
fetchId: string; | ||
} | ||
|
||
export const fetchRosRequest = createAction('ros/request')<RosActionMeta>(); | ||
export const fetchRosSuccess = createAction('ros/success')<Ros, RosActionMeta>(); | ||
export const fetchRosFailure = createAction('ros/failure')<AxiosError, RosActionMeta>(); | ||
|
||
export function fetchRos(rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string): ThunkAction { | ||
return (dispatch, getState) => { | ||
if (!isRosExpired(getState(), rosPathsType, rosType, rosQueryString)) { | ||
return; | ||
} | ||
|
||
const meta: RosActionMeta = { | ||
fetchId: getFetchId(rosPathsType, rosType, rosQueryString), | ||
}; | ||
|
||
dispatch(fetchRosRequest(meta)); | ||
runRos(rosPathsType, rosType, rosQueryString) | ||
.then(res => { | ||
// See https://github.com/project-koku/koku-ui/pull/580 | ||
// const repsonseData = dropCurrentMonthData(res, query); | ||
dispatch(fetchRosSuccess(res.data, meta)); | ||
}) | ||
.catch(err => { | ||
dispatch(fetchRosFailure(err, meta)); | ||
}); | ||
}; | ||
} | ||
|
||
function isRosExpired(state: RootState, rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string) { | ||
const ros = selectRos(state, rosPathsType, rosType, rosQueryString); | ||
const fetchStatus = selectRosFetchStatus(state, rosPathsType, rosType, rosQueryString); | ||
if (fetchStatus === FetchStatus.inProgress) { | ||
return false; | ||
} | ||
|
||
if (!ros) { | ||
return true; | ||
} | ||
|
||
const now = Date.now(); | ||
return now > ros.timeRequested + expirationMS; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import type { RosPathsType, RosType } from 'api/ros/ros'; | ||
export const rosStateKey = 'ros'; | ||
|
||
export function getFetchId(rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string) { | ||
return `${rosPathsType}--${rosType}--${rosQueryString}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import type { Ros } from 'api/ros/ros'; | ||
import type { AxiosError } from 'axios'; | ||
import { FetchStatus } from 'store/common'; | ||
import { resetState } from 'store/ui/uiActions'; | ||
import type { ActionType } from 'typesafe-actions'; | ||
import { getType } from 'typesafe-actions'; | ||
|
||
import { fetchRosFailure, fetchRosRequest, fetchRosSuccess } from './rosActions'; | ||
|
||
export interface CachedRos extends Ros { | ||
timeRequested: number; | ||
} | ||
|
||
export type RosState = Readonly<{ | ||
byId: Map<string, CachedRos>; | ||
fetchStatus: Map<string, FetchStatus>; | ||
errors: Map<string, AxiosError>; | ||
}>; | ||
|
||
const defaultState: RosState = { | ||
byId: new Map(), | ||
fetchStatus: new Map(), | ||
errors: new Map(), | ||
}; | ||
|
||
export type RosAction = ActionType< | ||
typeof fetchRosFailure | typeof fetchRosRequest | typeof fetchRosSuccess | typeof resetState | ||
>; | ||
|
||
export function rosReducer(state = defaultState, action: RosAction): RosState { | ||
switch (action.type) { | ||
case getType(resetState): | ||
state = defaultState; | ||
return state; | ||
|
||
case getType(fetchRosRequest): | ||
return { | ||
...state, | ||
fetchStatus: new Map(state.fetchStatus).set(action.payload.fetchId, FetchStatus.inProgress), | ||
}; | ||
|
||
case getType(fetchRosSuccess): | ||
return { | ||
...state, | ||
fetchStatus: new Map(state.fetchStatus).set(action.meta.fetchId, FetchStatus.complete), | ||
byId: new Map(state.byId).set(action.meta.fetchId, { | ||
...action.payload, | ||
timeRequested: Date.now(), | ||
}), | ||
errors: new Map(state.errors).set(action.meta.fetchId, null), | ||
}; | ||
|
||
case getType(fetchRosFailure): | ||
return { | ||
...state, | ||
fetchStatus: new Map(state.fetchStatus).set(action.meta.fetchId, FetchStatus.complete), | ||
errors: new Map(state.errors).set(action.meta.fetchId, action.payload), | ||
}; | ||
default: | ||
return state; | ||
} | ||
} |
Oops, something went wrong.