diff --git a/web-ui/Dockerfile b/web-ui/Dockerfile
index ac0aa0c1f..7e75eec1d 100644
--- a/web-ui/Dockerfile
+++ b/web-ui/Dockerfile
@@ -1,16 +1,30 @@
-FROM registry.opensource.zalan.do/library/node-18-alpine:latest
+FROM registry.opensource.zalan.do/library/node-18-alpine:latest AS builder
MAINTAINER "http://zalando.github.io/"
-RUN apk --no-cache add curl
-
-COPY package.json ./
-COPY yarn.lock ./
+WORKDIR /app
-RUN yarn --production
+COPY package.json yarn.lock ./
+RUN yarn install --frozen-lockfile
COPY src ./src
COPY server.js ./
+COPY webpack ./webpack
+COPY .babelrc ./
+
+RUN yarn build
+
+FROM registry.opensource.zalan.do/library/node-18-alpine:latest AS runtime
+
+WORKDIR /app
+
+RUN apk --no-cache add curl
+
+COPY package.json yarn.lock ./
+RUN yarn install --production --frozen-lockfile
+
+COPY --from=builder /app/src ./src
+COPY --from=builder /app/server.js ./
EXPOSE 3000
diff --git a/web-ui/src/client/app/components/__tests__/violations.test.jsx b/web-ui/src/client/app/components/__tests__/violations.test.jsx
index fa69aaa53..9e27fc207 100644
--- a/web-ui/src/client/app/components/__tests__/violations.test.jsx
+++ b/web-ui/src/client/app/components/__tests__/violations.test.jsx
@@ -53,6 +53,20 @@ describe('Violations component', () => {
const component = shallow();
expect(component.find('Violation')).toHaveLength(0);
});
+
+ test('should show MUST and SHOULD counts', () => {
+ const violationsCount = { must: 2, should: 3 };
+ const component = shallow(
+
+ );
+ const counts = component.find('.violations-heading__count');
+
+ expect(counts).toHaveLength(2);
+ expect(counts.at(0).text()).toContain('MUST:2');
+ expect(counts.at(1).text()).toContain('SHOULD:3');
+ expect(counts.at(0).find('RuleType')).toHaveLength(1);
+ expect(counts.at(1).find('RuleType')).toHaveLength(1);
+ });
});
describe('Violation component', () => {
@@ -168,13 +182,14 @@ describe('ViolationsResult component', () => {
describe('when state is complete with violations', () => {
test('should render violations', () => {
const violations = [{}, {}];
+ const violationsCount = { must: 1, should: 2 };
const component = shallow(
@@ -186,7 +201,7 @@ describe('ViolationsResult component', () => {
expect(Violations).toHaveLength(1);
expect(Violations.prop('violations')).toEqual(violations);
- expect(Violations.prop('violationsCount')).toEqual(2);
+ expect(Violations.prop('violationsCount')).toEqual(violationsCount);
});
});
});
diff --git a/web-ui/src/client/app/components/violations.jsx b/web-ui/src/client/app/components/violations.jsx
index 794c51ccb..00e0b9665 100644
--- a/web-ui/src/client/app/components/violations.jsx
+++ b/web-ui/src/client/app/components/violations.jsx
@@ -6,14 +6,42 @@ import FluidContainer from './fluid-container.jsx';
import { Link } from 'react-router-dom';
export function Violations(props) {
+ const {
+ externalId,
+ violations = [],
+ violationsCount = {},
+ } = props;
+
+ const parseCount = value =>
+ typeof value === 'number' ? value : 0;
+
+ const severityCounts = [
+ { label: 'MUST', value: parseCount(violationsCount.must) },
+ { label: 'SHOULD', value: parseCount(violationsCount.should) },
+ ];
+
return (
-
- VIOLATIONS
-
-
+
+ VIOLATIONS
+
+ {severityCounts.map(({ label, value }) => (
+
+ {label}:
+
+ {value}
+
+
+
+ ))}
+
+
+
@@ -25,7 +53,7 @@ export function Violations(props) {
- {props.violations.map((violation, index) => {
+ {violations.map((violation, index) => {
return ;
})}
diff --git a/web-ui/src/client/app/containers/__tests__/violations.test.jsx b/web-ui/src/client/app/containers/__tests__/violations.test.jsx
index 4e4a15a8e..f32aa666e 100644
--- a/web-ui/src/client/app/containers/__tests__/violations.test.jsx
+++ b/web-ui/src/client/app/containers/__tests__/violations.test.jsx
@@ -27,7 +27,7 @@ describe('Violations container component', () => {
describe('when call handleFormSubmit', () => {
test('should handle success', () => {
const violations = [{}];
- const violationsCount = 1;
+ const violationsCount = { must: 1, should: 0 };
getApiViolations.mockReturnValueOnce(
Promise.resolve({
violations: violations,
diff --git a/web-ui/src/client/app/containers/editor.jsx b/web-ui/src/client/app/containers/editor.jsx
index 41e1e8393..a2a7ca3dd 100644
--- a/web-ui/src/client/app/containers/editor.jsx
+++ b/web-ui/src/client/app/containers/editor.jsx
@@ -135,28 +135,30 @@ export class Editor extends Violations {
/>
) : null}
-
+
);
diff --git a/web-ui/src/client/app/containers/url.jsx b/web-ui/src/client/app/containers/url.jsx
index 4f8fea860..06bc194cf 100644
--- a/web-ui/src/client/app/containers/url.jsx
+++ b/web-ui/src/client/app/containers/url.jsx
@@ -62,6 +62,7 @@ export class URL extends Violations {
errorMsgText={this.state.error}
externalId={this.state.externalId}
violations={this.state.violations}
+ violationsCount={this.state.violationsCount}
successMsgTitle={this.state.successMsgTitle}
successMsgText={this.state.successMsgText}
/>
diff --git a/web-ui/src/client/app/index.scss b/web-ui/src/client/app/index.scss
index 2de4069a8..87e72a140 100644
--- a/web-ui/src/client/app/index.scss
+++ b/web-ui/src/client/app/index.scss
@@ -200,6 +200,50 @@ footer {
height: 90%;
}
+.violations-heading {
+ margin: 0 0 1rem;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 0.75rem;
+}
+
+.violations-heading__counts {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ font-size: 1rem;
+ color: #1f1f1f;
+}
+
+.violations-heading__count {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: inherit;
+ color: inherit;
+}
+
+.violations-heading__count strong {
+ font-size: inherit;
+ font-weight: 600;
+ color: inherit;
+}
+
+.violations-heading__count-value {
+ font-weight: 500;
+}
+
+.violations-heading__count + .violations-heading__count::before {
+ content: '/';
+ margin: 0 0.45rem;
+ color: #d1d1d1;
+}
+
+.violations-heading__link {
+ margin-left: auto;
+}
+
.dc-column {
height: 100%;
}
@@ -209,4 +253,3 @@ footer {
height: 80%;
}
}
-