Skip to content
Open
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
2 changes: 1 addition & 1 deletion dist/swagger-ui.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/swagger-ui.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/core/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const defaultOptions = Object.freeze({
validatorUrl: "https://validator.swagger.io/validator",
oauth2RedirectUrl: undefined,
persistAuthorization: false,
changeHistory: true,
changeHistoryMaxEntries: 20,
configs: {},
displayOperationId: false,
displayRequestDuration: false,
Expand Down
56 changes: 56 additions & 0 deletions src/core/containers/change-history.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react"
import PropTypes from "prop-types"

import ChangeHistorySidebar from "core/plugins/change-history/components/ChangeHistorySidebar"

export default class ChangeHistoryContainer extends React.Component {
static propTypes = {
changeHistorySelectors: PropTypes.object.isRequired,
changeHistoryActions: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired,
}

onClose = () => {
this.props.changeHistoryActions.setPanelOpen(false)
}

onClear = () => {
this.props.changeHistoryActions.clearHistory()
}

render() {
const { changeHistorySelectors, specSelectors, getComponent, getConfigs } =
this.props
const configs = getConfigs()

if (configs.changeHistory === false) {
return null
}

const isLoading = specSelectors.loadingStatus() === "loading"
const isPanelOpen = changeHistorySelectors.isPanelOpen()
const history = changeHistorySelectors.history()

if (isLoading || !isPanelOpen) {
return null
}

return (
<>
<div
className="change-history-backdrop"
onClick={this.onClose}
role="presentation"
/>
<ChangeHistorySidebar
history={history}
onClose={this.onClose}
onClear={this.onClear}
getComponent={getComponent}
/>
</>
)
}
}
32 changes: 32 additions & 0 deletions src/core/plugins/change-history/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const SET_HISTORY = "change_history_set_history"
export const SET_PANEL_OPEN = "change_history_set_panel_open"
export const SET_UNSEEN_CHANGES = "change_history_set_unseen_changes"
export const SET_STORAGE_KEY = "change_history_set_storage_key"

export function setHistory(history) {
return {
type: SET_HISTORY,
payload: history,
}
}

export function setPanelOpen(isOpen) {
return {
type: SET_PANEL_OPEN,
payload: isOpen,
}
}

export function setUnseenChanges(hasUnseenChanges) {
return {
type: SET_UNSEEN_CHANGES,
payload: hasUnseenChanges,
}
}

export function setStorageKey(storageKey) {
return {
type: SET_STORAGE_KEY,
payload: storageKey,
}
}
106 changes: 106 additions & 0 deletions src/core/plugins/change-history/components/ChangeHistorySidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from "react"
import PropTypes from "prop-types"

import { formatChangeSummary } from "core/plugins/change-history/fn"

class ChangeHistorySidebar extends React.Component {
static propTypes = {
history: PropTypes.object,
onClose: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired,
}

formatTimestamp(timestamp) {
return new Date(timestamp).toLocaleString()
}

renderChange(change, index) {
const isAddition = change.type.includes("added")
const isRemoval = change.type.includes("removed")
const className = [
"change-history-item",
isAddition ? "change-added" : "",
isRemoval ? "change-removed" : "",
!isAddition && !isRemoval ? "change-modified" : "",
]
.filter(Boolean)
.join(" ")

return (
<li key={`${change.type}-${index}`} className={className}>
{formatChangeSummary(change)}
</li>
)
}

renderEntry(entry, index) {
const changes = entry.changes || []
const isBaseline = entry.isBaseline
const timestamp = entry.timestamp
const version = entry.version
const title = entry.title

return (
<div key={entry.id || index} className="change-history-entry">
<div className="change-history-entry-header">
<span className="change-history-entry-time">
{this.formatTimestamp(timestamp)}
</span>
{version ? (
<span className="change-history-entry-version">v{version}</span>
) : null}
{title ? <span className="change-history-entry-title">{title}</span> : null}
</div>

{isBaseline ? (
<p className="change-history-baseline">Initial snapshot saved</p>
) : changes.length ? (
<ul className="change-history-changes">
{changes.map((change, changeIndex) =>
this.renderChange(change, changeIndex)
)}
</ul>
) : (
<p className="change-history-no-changes">No structural changes detected</p>
)}
</div>
)
}

render() {
const { history, onClose, onClear, getComponent } = this.props
const Button = getComponent("Button")
const entries = history?.toJS?.() || history || []

return (
<aside className="change-history-sidebar" aria-label="API change history">
<div className="change-history-sidebar-header">
<h4>API Change History</h4>
<div className="change-history-sidebar-actions">
{entries.length ? (
<Button className="btn change-history-clear-btn" onClick={onClear}>
Clear history
</Button>
) : null}
<Button className="btn change-history-close-btn" onClick={onClose}>
Close
</Button>
</div>
</div>

{!entries.length ? (
<p className="change-history-empty">
No history yet. Reload the spec after backend changes to capture diffs.
</p>
) : (
<div className="change-history-entries">
{entries.map((entry, index) => this.renderEntry(entry, index))}
</div>
)}
</aside>
)
}
}

export default ChangeHistorySidebar
Loading