Skip to content

Commit cdaadad

Browse files
committed
feat (change-history): add sidebar with topbar toggle
1 parent 832008a commit cdaadad

26 files changed

Lines changed: 2036 additions & 6 deletions

File tree

dist/swagger-ui.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/swagger-ui.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/config/defaults.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const defaultOptions = Object.freeze({
1717
validatorUrl: "https://validator.swagger.io/validator",
1818
oauth2RedirectUrl: undefined,
1919
persistAuthorization: false,
20+
changeHistory: true,
21+
changeHistoryMaxEntries: 20,
2022
configs: {},
2123
displayOperationId: false,
2224
displayRequestDuration: false,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from "react"
2+
import PropTypes from "prop-types"
3+
4+
import ChangeHistorySidebar from "core/plugins/change-history/components/ChangeHistorySidebar"
5+
6+
export default class ChangeHistoryContainer extends React.Component {
7+
static propTypes = {
8+
changeHistorySelectors: PropTypes.object.isRequired,
9+
changeHistoryActions: PropTypes.object.isRequired,
10+
specSelectors: PropTypes.object.isRequired,
11+
getComponent: PropTypes.func.isRequired,
12+
getConfigs: PropTypes.func.isRequired,
13+
}
14+
15+
onClose = () => {
16+
this.props.changeHistoryActions.setPanelOpen(false)
17+
}
18+
19+
onClear = () => {
20+
this.props.changeHistoryActions.clearHistory()
21+
}
22+
23+
render() {
24+
const { changeHistorySelectors, specSelectors, getComponent, getConfigs } =
25+
this.props
26+
const configs = getConfigs()
27+
28+
if (configs.changeHistory === false) {
29+
return null
30+
}
31+
32+
const isLoading = specSelectors.loadingStatus() === "loading"
33+
const isPanelOpen = changeHistorySelectors.isPanelOpen()
34+
const history = changeHistorySelectors.history()
35+
36+
if (isLoading || !isPanelOpen) {
37+
return null
38+
}
39+
40+
return (
41+
<>
42+
<div
43+
className="change-history-backdrop"
44+
onClick={this.onClose}
45+
role="presentation"
46+
/>
47+
<ChangeHistorySidebar
48+
history={history}
49+
onClose={this.onClose}
50+
onClear={this.onClear}
51+
getComponent={getComponent}
52+
/>
53+
</>
54+
)
55+
}
56+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const SET_HISTORY = "change_history_set_history"
2+
export const SET_PANEL_OPEN = "change_history_set_panel_open"
3+
export const SET_UNSEEN_CHANGES = "change_history_set_unseen_changes"
4+
export const SET_STORAGE_KEY = "change_history_set_storage_key"
5+
6+
export function setHistory(history) {
7+
return {
8+
type: SET_HISTORY,
9+
payload: history,
10+
}
11+
}
12+
13+
export function setPanelOpen(isOpen) {
14+
return {
15+
type: SET_PANEL_OPEN,
16+
payload: isOpen,
17+
}
18+
}
19+
20+
export function setUnseenChanges(hasUnseenChanges) {
21+
return {
22+
type: SET_UNSEEN_CHANGES,
23+
payload: hasUnseenChanges,
24+
}
25+
}
26+
27+
export function setStorageKey(storageKey) {
28+
return {
29+
type: SET_STORAGE_KEY,
30+
payload: storageKey,
31+
}
32+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React from "react"
2+
import PropTypes from "prop-types"
3+
4+
import { formatChangeSummary } from "core/plugins/change-history/fn"
5+
6+
class ChangeHistorySidebar extends React.Component {
7+
static propTypes = {
8+
history: PropTypes.object,
9+
onClose: PropTypes.func.isRequired,
10+
onClear: PropTypes.func.isRequired,
11+
getComponent: PropTypes.func.isRequired,
12+
}
13+
14+
formatTimestamp(timestamp) {
15+
return new Date(timestamp).toLocaleString()
16+
}
17+
18+
renderChange(change, index) {
19+
const isAddition = change.type.includes("added")
20+
const isRemoval = change.type.includes("removed")
21+
const className = [
22+
"change-history-item",
23+
isAddition ? "change-added" : "",
24+
isRemoval ? "change-removed" : "",
25+
!isAddition && !isRemoval ? "change-modified" : "",
26+
]
27+
.filter(Boolean)
28+
.join(" ")
29+
30+
return (
31+
<li key={`${change.type}-${index}`} className={className}>
32+
{formatChangeSummary(change)}
33+
</li>
34+
)
35+
}
36+
37+
renderEntry(entry, index) {
38+
const changes = entry.changes || []
39+
const isBaseline = entry.isBaseline
40+
const timestamp = entry.timestamp
41+
const version = entry.version
42+
const title = entry.title
43+
44+
return (
45+
<div key={entry.id || index} className="change-history-entry">
46+
<div className="change-history-entry-header">
47+
<span className="change-history-entry-time">
48+
{this.formatTimestamp(timestamp)}
49+
</span>
50+
{version ? (
51+
<span className="change-history-entry-version">v{version}</span>
52+
) : null}
53+
{title ? <span className="change-history-entry-title">{title}</span> : null}
54+
</div>
55+
56+
{isBaseline ? (
57+
<p className="change-history-baseline">Initial snapshot saved</p>
58+
) : changes.length ? (
59+
<ul className="change-history-changes">
60+
{changes.map((change, changeIndex) =>
61+
this.renderChange(change, changeIndex)
62+
)}
63+
</ul>
64+
) : (
65+
<p className="change-history-no-changes">No structural changes detected</p>
66+
)}
67+
</div>
68+
)
69+
}
70+
71+
render() {
72+
const { history, onClose, onClear, getComponent } = this.props
73+
const Button = getComponent("Button")
74+
const entries = history?.toJS?.() || history || []
75+
76+
return (
77+
<aside className="change-history-sidebar" aria-label="API change history">
78+
<div className="change-history-sidebar-header">
79+
<h4>API Change History</h4>
80+
<div className="change-history-sidebar-actions">
81+
{entries.length ? (
82+
<Button className="btn change-history-clear-btn" onClick={onClear}>
83+
Clear history
84+
</Button>
85+
) : null}
86+
<Button className="btn change-history-close-btn" onClick={onClose}>
87+
Close
88+
</Button>
89+
</div>
90+
</div>
91+
92+
{!entries.length ? (
93+
<p className="change-history-empty">
94+
No history yet. Reload the spec after backend changes to capture diffs.
95+
</p>
96+
) : (
97+
<div className="change-history-entries">
98+
{entries.map((entry, index) => this.renderEntry(entry, index))}
99+
</div>
100+
)}
101+
</aside>
102+
)
103+
}
104+
}
105+
106+
export default ChangeHistorySidebar

0 commit comments

Comments
 (0)