@konfuzio/document-validation-ui 0.1.48-dev.2 → 0.1.48-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/jest.config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
2
  preset: "@vue/cli-plugin-unit-jest/presets/no-babel",
3
3
  setupFiles: ["./tests/setup.js"],
4
- transformIgnorePatterns: ["node_modules/(?!axios)"],
4
+ transformIgnorePatterns: ["node_modules/(?!axios|keycloak-js)"],
5
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konfuzio/document-validation-ui",
3
- "version": "0.1.48-dev.2",
3
+ "version": "0.1.48-dev.3",
4
4
  "repository": "git://github.com:konfuzio-ai/document-validation-ui.git",
5
5
  "main": "dist/app.js",
6
6
  "scripts": {
@@ -32,6 +32,7 @@
32
32
  "axios": "^1.7.4",
33
33
  "bignumber.js": "^9.1.0",
34
34
  "buefy": "^0.9.22",
35
+ "keycloak-js": "^26.0.6",
35
36
  "konva": "^8.3.13",
36
37
  "sass": "^1.56.0",
37
38
  "sass-loader": "^13.1.0",
package/src/api.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import axios from "axios";
2
+ import { updateKeycloakToken } from "./utils/keycloak";
2
3
 
3
- let HTTP, FILE_REQUEST, authToken, appLocale;
4
+ let HTTP, FILE_REQUEST, authToken, appLocale, isKeycloakAuth;
4
5
  const DEFAULT_URL = "https://app.konfuzio.com";
5
6
  const FILE_URL = process.env.VUE_APP_IMAGE_URL;
6
7
 
7
- axios.defaults.xsrfCookieName = "csrftoken";
8
- axios.defaults.xsrfHeaderName = "X-CSRFToken";
9
-
10
8
  HTTP = axios.create({
11
9
  baseURL: process.env.VUE_APP_API_URL || `${DEFAULT_URL}/api/v3/`,
12
10
  });
@@ -20,6 +18,10 @@ const setAuthToken = (token) => {
20
18
  authToken = token;
21
19
  };
22
20
 
21
+ const setIsKeycloakAuth = (result) => {
22
+ isKeycloakAuth = result;
23
+ };
24
+
23
25
  const setApiUrl = (url) => {
24
26
  HTTP.defaults.baseURL = url;
25
27
  };
@@ -32,15 +34,22 @@ const setLocale = (locale) => {
32
34
  appLocale = locale;
33
35
  };
34
36
 
35
- const getInterceptorConfig = (config) => {
37
+ const getInterceptorConfig = async (config) => {
36
38
  if (authToken) {
37
- config.headers["Authorization"] = `Token ${authToken}`;
38
- config.headers["Accept-Language"] = `${appLocale}-${appLocale}`;
39
+ config.headers["Authorization"] = `${
40
+ isKeycloakAuth ? "Bearer" : "Token"
41
+ } ${authToken}`;
39
42
  }
43
+ config.headers["Accept-Language"] = `${appLocale}-${appLocale}`;
44
+
45
+ if (isKeycloakAuth) {
46
+ await updateKeycloakToken();
47
+ }
48
+
40
49
  return config;
41
50
  };
42
51
 
43
- HTTP.interceptors.request.use(getInterceptorConfig, (error) => {
52
+ HTTP.interceptors.request.use(getInterceptorConfig, async (error) => {
44
53
  return Promise.reject(error);
45
54
  });
46
55
 
@@ -107,6 +116,7 @@ export default {
107
116
  makeFileRequest,
108
117
  makeGetPaginatedRequest,
109
118
  setAuthToken,
119
+ setIsKeycloakAuth,
110
120
  setLocale,
111
121
  FILE_REQUEST,
112
122
  DEFAULT_URL,
@@ -9,4 +9,14 @@
9
9
  &.default-cursor {
10
10
  cursor: default;
11
11
  }
12
+
13
+ .annotation-label {
14
+ position: absolute;
15
+ z-index: 999;
16
+ background-color: $text-color;
17
+ color: $white;
18
+ font-size: 12px;
19
+ padding: 4px;
20
+ word-break: break-all;
21
+ }
12
22
  }
@@ -90,6 +90,15 @@
90
90
  &.is-text {
91
91
  text-decoration: none;
92
92
 
93
+ &.has-right-icon {
94
+ > span {
95
+ padding-right: 16px;
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ }
100
+ }
101
+
93
102
  &:hover {
94
103
  background-color: transparent;
95
104
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from "../utils/utils";
20
20
  import { Integrations } from "@sentry/tracing";
21
21
  import API from "../api";
22
+ import { initKeycloak } from "../utils/keycloak";
22
23
 
23
24
  export default {
24
25
  name: "App",
@@ -98,6 +99,24 @@ export default {
98
99
  required: false,
99
100
  default: "",
100
101
  },
102
+ // eslint-disable-next-line vue/prop-name-casing
103
+ sso_url: {
104
+ type: String,
105
+ required: false,
106
+ default: "",
107
+ },
108
+ // eslint-disable-next-line vue/prop-name-casing
109
+ sso_realm: {
110
+ type: String,
111
+ required: false,
112
+ default: "",
113
+ },
114
+ // eslint-disable-next-line vue/prop-name-casing
115
+ sso_client_id: {
116
+ type: String,
117
+ required: false,
118
+ default: "",
119
+ },
101
120
  },
102
121
  computed: {
103
122
  ...mapState("display", ["pageError"]),
@@ -133,7 +152,11 @@ export default {
133
152
  }
134
153
  },
135
154
  isPublicView() {
136
- if (this.userToken || this.fullMode) {
155
+ if (
156
+ this.userToken ||
157
+ this.fullMode ||
158
+ (this.ssoUrl && this.ssoRealm && this.ssoClientId)
159
+ ) {
137
160
  return false;
138
161
  } else {
139
162
  return true;
@@ -160,6 +183,33 @@ export default {
160
183
  return null;
161
184
  }
162
185
  },
186
+ ssoUrl() {
187
+ if (process.env.VUE_APP_SSO_URL) {
188
+ return process.env.VUE_APP_SSO_URL;
189
+ } else if (this.sso_url) {
190
+ return this.sso_url;
191
+ } else {
192
+ return null;
193
+ }
194
+ },
195
+ ssoRealm() {
196
+ if (process.env.VUE_APP_SSO_REALM) {
197
+ return process.env.VUE_APP_SSO_REALM;
198
+ } else if (this.sso_realm) {
199
+ return this.sso_realm;
200
+ } else {
201
+ return null;
202
+ }
203
+ },
204
+ ssoClientId() {
205
+ if (process.env.VUE_APP_SSO_CLIENT_ID) {
206
+ return process.env.VUE_APP_SSO_CLIENT_ID;
207
+ } else if (this.sso_client_id) {
208
+ return this.sso_client_id;
209
+ } else {
210
+ return null;
211
+ }
212
+ },
163
213
  annotationId() {
164
214
  if (getURLValueFromHash("ann")) {
165
215
  return getURLValueFromHash("ann");
@@ -183,7 +233,7 @@ export default {
183
233
  }
184
234
  },
185
235
  },
186
- created() {
236
+ async created() {
187
237
  // Sentry config
188
238
  if (process.env.NODE_ENV != "development") {
189
239
  Sentry.init({
@@ -211,7 +261,12 @@ export default {
211
261
  }
212
262
 
213
263
  // api config
214
- API.setAuthToken(this.userToken);
264
+ if (this.userToken) {
265
+ API.setAuthToken(this.userToken);
266
+ } else if (this.ssoUrl && this.ssoRealm && this.ssoClientId) {
267
+ await initKeycloak(this.ssoUrl, this.ssoRealm, this.ssoClientId);
268
+ }
269
+
215
270
  API.setLocale(this.$i18n.locale);
216
271
 
217
272
  if (this.api_url !== "") {
@@ -31,6 +31,14 @@
31
31
  :container-height="scaledViewport.height"
32
32
  />
33
33
 
34
+ <div
35
+ v-if="showAnnotationLabel"
36
+ class="annotation-label"
37
+ :style="getAnnotationLabelPosition(showAnnotationLabel)"
38
+ >
39
+ {{ showAnnotationLabel.labelName }}
40
+ </div>
41
+
34
42
  <AnnSetTableOptions v-if="showAnnSetTable" :page="page" />
35
43
 
36
44
  <v-stage
@@ -78,35 +86,6 @@
78
86
  )"
79
87
  >
80
88
  <v-group :key="'ann' + annotation.id + '-' + index">
81
- <v-label
82
- v-if="annotation.id == annotationId && !searchEnabled"
83
- :key="`label${annotation.id}`"
84
- :config="{
85
- listening: false,
86
- ...annotationLabelRect(
87
- bbox,
88
- labelOfAnnotation(annotation).name
89
- ),
90
- }"
91
- >
92
- <v-tag
93
- :config="{
94
- fill: '#1A1A1A',
95
- lineJoin: 'round',
96
- hitStrokeWidth: 0,
97
- listening: false,
98
- }"
99
- />
100
- <v-text
101
- :config="{
102
- padding: 4,
103
- text: labelOfAnnotation(annotation).name,
104
- fill: 'white',
105
- fontSize: 12,
106
- listening: false,
107
- }"
108
- />
109
- </v-label>
110
89
  <v-rect
111
90
  v-if="!isAnnotationInEditMode(annotation.id)"
112
91
  :config="annotationRect(bbox, annotation.id)"
@@ -142,42 +121,6 @@
142
121
  </template>
143
122
  </template>
144
123
  </v-layer>
145
- <v-layer
146
- v-if="
147
- showFocusedAnnotation &&
148
- !isSelecting &&
149
- documentAnnotationSelected.labelName !== ''
150
- "
151
- >
152
- <v-label
153
- :key="`label${documentAnnotationSelected.id}`"
154
- :config="{
155
- listening: false,
156
- ...annotationLabelRect(
157
- documentAnnotationSelected.span,
158
- documentAnnotationSelected.labelName
159
- ),
160
- }"
161
- >
162
- <v-tag
163
- :config="{
164
- fill: '#1A1A1A',
165
- lineJoin: 'round',
166
- hitStrokeWidth: 0,
167
- listening: false,
168
- }"
169
- />
170
- <v-text
171
- :config="{
172
- padding: 4,
173
- text: documentAnnotationSelected.labelName,
174
- fill: 'white',
175
- fontSize: 12,
176
- listening: false,
177
- }"
178
- />
179
- </v-label>
180
- </v-layer>
181
124
  <v-layer v-if="page.number === selectionPage">
182
125
  <box-selection
183
126
  :page="page"
@@ -337,6 +280,18 @@ export default {
337
280
  this.page.number
338
281
  );
339
282
  },
283
+ showAnnotationLabel() {
284
+ if (
285
+ this.showFocusedAnnotation &&
286
+ !this.isSelecting &&
287
+ this.documentAnnotationSelected &&
288
+ this.documentAnnotationSelected.labelName !== ""
289
+ ) {
290
+ return this.documentAnnotationSelected;
291
+ } else {
292
+ return null;
293
+ }
294
+ },
340
295
  },
341
296
  watch: {
342
297
  recalculatingAnnotations(newState) {
@@ -686,22 +641,28 @@ export default {
686
641
  ...this.bboxToRect(this.page, bbox),
687
642
  };
688
643
  },
689
- /**
690
- * Builds the konva config object for the annotation label.
691
- */
692
- annotationLabelRect(bbox, labelName) {
693
- const rect = this.bboxToRect(this.page, bbox, true);
694
-
695
- // calculations to check if label name will go off document
696
- const calculatedX =
697
- rect.x + labelName.length * 5.4 < this.scaledViewport.width
698
- ? rect.x
699
- : this.scaledViewport.width - labelName.length * 5.4;
700
-
701
- return {
702
- x: calculatedX,
703
- y: rect.y,
704
- };
644
+ getAnnotationLabelPosition(annotation) {
645
+ if (annotation && this.$refs.stage) {
646
+ const padding = 8;
647
+ const maxCharacters = 10;
648
+ const minimumSpaceTopY = 50;
649
+ const rect = this.bboxToRect(this.page, annotation.span, true);
650
+
651
+ if (
652
+ annotation.labelName.length > maxCharacters &&
653
+ rect.y < minimumSpaceTopY
654
+ ) {
655
+ return `left: ${rect.x}px; top: ${
656
+ rect.y + rect.height * 3 + padding
657
+ }px`;
658
+ } else {
659
+ return `left: ${rect.x}px; bottom: ${
660
+ this.$refs.stage.$el.clientHeight - rect.y - rect.height - padding
661
+ }px`;
662
+ }
663
+ } else {
664
+ return "";
665
+ }
705
666
  },
706
667
  closePopups() {
707
668
  this.newAnnotation = [];
@@ -10,13 +10,18 @@
10
10
  :class="[
11
11
  'annotation-dropdown',
12
12
  'no-padding-bottom',
13
+ 'dropdown-full-width',
13
14
  setsList.length === 0 ? 'no-padding-top' : '',
14
15
  ]"
15
16
  scrollable
16
17
  >
17
18
  <template #trigger>
18
19
  <b-button
19
- :class="['popup-input', selectedSet ? '' : 'not-selected']"
20
+ :class="[
21
+ 'popup-input',
22
+ selectedSet ? '' : 'not-selected',
23
+ 'has-right-icon',
24
+ ]"
20
25
  type="is-text"
21
26
  >
22
27
  {{
@@ -72,11 +77,11 @@
72
77
  aria-role="list"
73
78
  :disabled="!labelsFiltered || labelsFiltered.length === 0"
74
79
  scrollable
75
- class="label-dropdown annotation-dropdown"
80
+ class="label-dropdown annotation-dropdown dropdown-full-width"
76
81
  >
77
82
  <template #trigger>
78
83
  <b-button
79
- class="popup-input"
84
+ class="popup-input has-right-icon"
80
85
  :disabled="!labelsFiltered"
81
86
  type="is-text"
82
87
  >
@@ -12,13 +12,18 @@
12
12
  'annotation-dropdown',
13
13
  'no-padding-bottom',
14
14
  'no-padding-top',
15
+ 'dropdown-full-width',
15
16
  setsList.length === 0 ? 'no-padding-top' : '',
16
17
  ]"
17
18
  scrollable
18
19
  >
19
20
  <template #trigger>
20
21
  <b-button
21
- :class="['popup-input', selectedSet ? '' : 'not-selected']"
22
+ :class="[
23
+ 'popup-input',
24
+ selectedSet ? '' : 'not-selected',
25
+ 'has-right-icon',
26
+ ]"
22
27
  type="is-text"
23
28
  >
24
29
  {{
@@ -76,11 +81,15 @@
76
81
  aria-role="list"
77
82
  :disabled="!textFromEntities || !labels || labels.length === 0"
78
83
  scrollable
79
- class="label-dropdown annotation-dropdown"
84
+ class="label-dropdown annotation-dropdown dropdown-full-width"
80
85
  >
81
86
  <template #trigger>
82
87
  <b-button
83
- :class="['popup-input', selectedLabel ? '' : 'not-selected']"
88
+ :class="[
89
+ 'popup-input',
90
+ selectedLabel ? '' : 'not-selected',
91
+ 'has-right-icon',
92
+ ]"
84
93
  type="is-text"
85
94
  >
86
95
  {{
@@ -706,7 +706,10 @@ const getters = {
706
706
  },
707
707
 
708
708
  annotationById: (state) => (annotationId) => {
709
- return state.annotations.find((ann) => ann.id == annotationId);
709
+ if (state.annotations) {
710
+ return state.annotations.find((ann) => ann.id == annotationId);
711
+ }
712
+ return null;
710
713
  },
711
714
 
712
715
  // Check if document is ready to be finished
@@ -954,7 +957,7 @@ const actions = {
954
957
  commit("SET_PAGES", []);
955
958
  commit("SET_DOC_ID", id);
956
959
  },
957
- setAnnotationId: ({ commit }, id) => {
960
+ setAnnotationId: ({ commit, dispatch, getters }, id) => {
958
961
  commit("SET_ANNOTATION_ID", id);
959
962
  setURLAnnotationHash(id);
960
963
  },
@@ -0,0 +1,38 @@
1
+ import Keycloak from "keycloak-js";
2
+ import API from "../api";
3
+
4
+ let keycloak;
5
+
6
+ export const initKeycloak = async (url, realm, clientId) => {
7
+ keycloak = new Keycloak({
8
+ url,
9
+ realm,
10
+ clientId,
11
+ });
12
+
13
+ try {
14
+ const authenticated = await keycloak.init({
15
+ onLoad: "login-required",
16
+ enableLogging: true,
17
+ });
18
+ if (authenticated) {
19
+ API.setIsKeycloakAuth(true);
20
+ API.setAuthToken(keycloak.token);
21
+ } else {
22
+ console.error("User is not authenticated");
23
+ }
24
+ } catch (error) {
25
+ console.error("Failed to initialize adapter:", error);
26
+ }
27
+ };
28
+
29
+ export const updateKeycloakToken = () => {
30
+ return new Promise(async (resolve, reject) => {
31
+ if (keycloak) {
32
+ const update = await keycloak.updateToken(30);
33
+ resolve();
34
+ } else {
35
+ reject();
36
+ }
37
+ });
38
+ };