Skip to content

Commit 7f872fe

Browse files
authored
Merge pull request #984 from zalando/feature/gh-593-permalinks
Permalinks UI
2 parents 41cef82 + d6a5227 commit 7f872fe

File tree

15 files changed

+133
-9
lines changed

15 files changed

+133
-9
lines changed

server/src/main/java/de/zalando/zally/apireview/ApiReview.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import javax.persistence.OneToMany
2525
class ApiReview(
2626
request: ApiDefinitionRequest,
2727
val userAgent: String = "",
28-
@Suppress("CanBeParameter") private val apiDefinition: String,
28+
@Suppress("CanBeParameter") val apiDefinition: String,
2929
violations: List<Result> = emptyList(),
3030
val name: String? = OpenApiHelper.extractApiName(apiDefinition),
3131
val apiId: String? = OpenApiHelper.extractApiId(apiDefinition),

server/src/main/java/de/zalando/zally/apireview/ApiViolationsController.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class ApiViolationsController(
101101
Severity.SHOULD to review.shouldViolations,
102102
Severity.MAY to review.mayViolations,
103103
Severity.HINT to review.hintViolations
104-
).map { it.first.name.toLowerCase() to it.second }.toMap()
104+
).map { it.first.name.toLowerCase() to it.second }.toMap(),
105+
apiDefinition = review.apiDefinition
105106
)
106107
}

server/src/main/java/de/zalando/zally/dto/ApiDefinitionResponse.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ data class ApiDefinitionResponse(
66
val externalId: UUID? = null,
77
val message: String? = null,
88
val violations: List<ViolationDTO> = emptyList(),
9-
val violationsCount: Map<String, Int> = emptyMap()
9+
val violationsCount: Map<String, Int> = emptyMap(),
10+
val apiDefinition: String?
1011
)

server/src/main/resources/api/zally-api.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ components:
288288
description: Custom server message
289289
violations_count:
290290
$ref: '#/components/schemas/ViolationsCount'
291+
api_definition:
292+
type: string
293+
description: Specification object in OpenAPI format
291294

292295
SupportedRulesResponse:
293296
type: object

server/src/test/java/de/zalando/zally/apireview/ApiViolationsControllerTest.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,24 @@ package de.zalando.zally.apireview
22

33
import de.zalando.zally.configuration.JacksonObjectMapperConfiguration
44
import de.zalando.zally.dto.ApiDefinitionRequest
5+
import org.hamcrest.CoreMatchers.hasItem
56
import org.hamcrest.Matchers.containsString
7+
import org.hamcrest.Matchers.notNullValue
68
import org.intellij.lang.annotations.Language
79
import org.junit.Test
810
import org.junit.runner.RunWith
911
import org.springframework.beans.factory.annotation.Autowired
1012
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
1113
import org.springframework.boot.test.context.SpringBootTest
14+
import org.springframework.http.MediaType.APPLICATION_JSON_UTF8
1215
import org.springframework.test.context.ActiveProfiles
1316
import org.springframework.test.context.junit4.SpringRunner
1417
import org.springframework.test.web.servlet.MockMvc
1518
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
1619
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
1720
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
21+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
22+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
1823
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
1924

2025
@RunWith(SpringRunner::class)
@@ -36,7 +41,13 @@ class ApiViolationsControllerTest {
3641
.content("{\"api_definition_string\":\"\"}")
3742
)
3843
.andExpect(status().isOk)
44+
.andExpect(header().exists("Location"))
45+
.andExpect(content().contentType(APPLICATION_JSON_UTF8))
3946
.andExpect(content().string(containsString("https://zalando.github.io/restful-api-guidelines")))
47+
.andExpect(jsonPath("$.violations[*].rule_link", hasItem("https://zalando.github.io/restful-api-guidelines/#101")))
48+
.andExpect(jsonPath("$.external_id", notNullValue()))
49+
.andExpect(jsonPath("$.violations", notNullValue()))
50+
.andExpect(jsonPath("$.api_definition", notNullValue()))
4051
}
4152

4253
@Test
@@ -49,7 +60,7 @@ class ApiViolationsControllerTest {
4960
}
5061

5162
@Test
52-
fun `getExistingViolationResponse with existing responds NotFound`() {
63+
fun `getExistingViolationResponse with existing responds Ok`() {
5364

5465
val location = mvc.perform(
5566
post("/api-violations")
@@ -64,7 +75,10 @@ class ApiViolationsControllerTest {
6475
.accept("application/json")
6576
)
6677
.andExpect(status().isOk)
67-
.andExpect(content().string(containsString("https://zalando.github.io/restful-api-guidelines")))
78+
.andExpect(content().contentType(APPLICATION_JSON_UTF8))
79+
.andExpect(jsonPath("$.external_id", notNullValue()))
80+
.andExpect(jsonPath("$.violations", notNullValue()))
81+
.andExpect(jsonPath("$.api_definition", notNullValue()))
6882
}
6983

7084
/**

web-ui/src/client/app/components/__tests__/violations.test.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ describe('Violations component', () => {
5252
test('should render empty list of violation', () => {
5353
const component = shallow(<Violations violations={[]} />);
5454
expect(component.find('Violation')).toHaveLength(0);
55-
expect(component.find('h3')).toHaveLength(0);
5655
});
5756
});
5857

web-ui/src/client/app/components/violations.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { If } from './util.jsx';
33
import { Msg } from './msg.jsx';
44
import { RuleType } from './rules.jsx';
55
import FluidContainer from './fluid-container.jsx';
6+
import { Link } from 'react-router-dom';
67

78
export function Violations(props) {
89
return (
910
<div>
1011
<div className="dc-row">
1112
<div className="dc-column">
12-
{props.violations.length ? <h3>VIOLATIONS</h3> : ''}
13+
<h3>
14+
VIOLATIONS
15+
<span style={{ float: 'right' }}>
16+
<Link to={'/editor/' + props.externalId} className="dc-link">
17+
<i className="dc-icon dc-icon--interactive dc-icon--link" />
18+
</Link>
19+
</span>
20+
</h3>
1321
</div>
1422
</div>
1523
<div className="violations-container">
@@ -136,6 +144,7 @@ export function ViolationsResult(props) {
136144
dataTestId="if-violations"
137145
>
138146
<Violations
147+
externalId={props.externalId}
139148
violations={props.violations}
140149
violationsCount={props.violationsCount}
141150
/>

web-ui/src/client/app/containers/__tests__/violations-tab.test.jsx.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ describe('ViolationsTab container component', () => {
4343
expect(component.find('Editor')).toHaveLength(1);
4444
expect(component.find('Rules')).toHaveLength(0);
4545
});
46+
test('render the Editor tab on /editor/:externalId', () => {
47+
const component = mount(
48+
<StaticRouter location={{ pathname: '/editor/:externalId' }}>
49+
<ViolationsTab authenticated />
50+
</StaticRouter>
51+
);
52+
expect(component.find('URL')).toHaveLength(0);
53+
expect(component.find('Editor')).toHaveLength(1);
54+
expect(component.find('Rules')).toHaveLength(0);
55+
});
4656
test('render the Rules tab on /rules', () => {
4757
const component = mount(
4858
<StaticRouter location={{ pathname: '/rules' }}>

web-ui/src/client/app/containers/app.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function App(props) {
1414
Storage,
1515
getApiViolationsByURL,
1616
getApiViolationsBySchema,
17+
getApiViolationsByExternalId,
1718
getSupportedRules,
1819
getFile,
1920
} = props;
@@ -58,6 +59,7 @@ export function App(props) {
5859
getSupportedRules={getSupportedRules}
5960
getApiViolationsByURL={getApiViolationsByURL}
6061
getApiViolationsBySchema={getApiViolationsBySchema}
62+
getApiViolationsByExternalId={getApiViolationsByExternalId}
6163
getFile={getFile}
6264
Storage={Storage}
6365
{...props}

web-ui/src/client/app/containers/editor.jsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Violations } from './violations.jsx';
44
import { ViolationsResult } from '../components/violations.jsx';
55
import { EditorInputForm } from '../components/editor.jsx';
66
import { Dialog } from '../components/dialog.jsx';
7+
import { id } from 'brace/worker/json';
78

89
export const editorErrorToAnnotations = error => {
910
if (!error || !error.mark) {
@@ -22,15 +23,58 @@ export const editorErrorToAnnotations = error => {
2223
export class Editor extends Violations {
2324
constructor(props) {
2425
super(props);
26+
if (
27+
props &&
28+
props.match &&
29+
props.match.params &&
30+
props.match.params.externalId
31+
) {
32+
this.state.externalId = props.match.params.externalId;
33+
} else {
34+
this.state.externalId = null;
35+
}
2536
this.state.editorDirty = true;
26-
this.state.editorValue = this.Storage.getItem('editor-value') || '';
37+
this.state.editorValue =
38+
(!this.state.externalId && this.Storage.getItem('editor-value')) || '';
2739
this.handleOnInputValueChange = this.handleOnInputValueChange.bind(this);
2840
this.handleFormSubmit = this.handleFormSubmit.bind(this);
2941
this.handleHideOverlay = this.handleHideOverlay.bind(this);
3042
}
3143

3244
componentDidMount() {
33-
this.updateInputValue(this.state.editorValue);
45+
if (this.state.externalId) {
46+
this.getApiViolationsByExternalId(this.state.externalId)
47+
.then(response => {
48+
this.setState({
49+
pending: false,
50+
ajaxComplete: true,
51+
externalId: response.external_id,
52+
violations: response.violations,
53+
violationsCount: response.violations_count,
54+
editorValue: response.api_definition,
55+
});
56+
this.Storage.setItem('editor-value', this.state.editorValue);
57+
this.updateInputValue(this.state.editorValue);
58+
})
59+
.catch(error => {
60+
console.error(error); // eslint-disable-line no-console
61+
this.setState({
62+
pending: false,
63+
ajaxComplete: true,
64+
error: error.detail || Violations.DEFAULT_ERROR_MESSAGE,
65+
violations: [],
66+
violationsCount: {
67+
could: 0,
68+
hint: 0,
69+
must: 0,
70+
should: 0,
71+
},
72+
});
73+
return Promise.reject(error);
74+
});
75+
} else {
76+
this.updateInputValue(this.state.editorValue);
77+
}
3478
}
3579

3680
handleFormSubmit(event) {
@@ -95,6 +139,7 @@ export class Editor extends Violations {
95139
pending={this.state.pending}
96140
complete={this.state.ajaxComplete}
97141
errorMsgText={this.state.error}
142+
externalId={this.state.externalId}
98143
violations={this.state.violations}
99144
successMsgTitle={this.state.successMsgTitle}
100145
successMsgText={this.state.successMsgText}
@@ -107,6 +152,7 @@ export class Editor extends Violations {
107152
pending={this.state.pending}
108153
complete={this.state.ajaxComplete}
109154
errorMsgText={this.state.error}
155+
externalId={this.state.externalId}
110156
violations={this.state.violations}
111157
successMsgTitle={this.state.successMsgTitle}
112158
successMsgText={this.state.successMsgText}

0 commit comments

Comments
 (0)