@konfuzio/document-validation-ui 0.1.48-dev.1 → 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.1",
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
  {{
@@ -24,7 +29,9 @@
24
29
  ? `${selectedSet.label_set.name} ${
25
30
  selectedSet.id
26
31
  ? numberOfAnnotationSetGroup(selectedSet)
27
- : `(${$t("new")})`
32
+ : `${numberOfLabelSetGroup(selectedSet.label_set)} (${$t(
33
+ "new"
34
+ )})`
28
35
  }`
29
36
  : $t("select_annotation_set")
30
37
  }}
@@ -33,6 +40,14 @@
33
40
  </span>
34
41
  </b-button>
35
42
  </template>
43
+ <b-button
44
+ type="is-ghost"
45
+ :class="['add-ann-set', 'dropdown-item', 'no-icon-margin']"
46
+ icon-left="plus"
47
+ @click="openAnnotationSetCreation"
48
+ >
49
+ {{ $t("new_ann_set_title") }}
50
+ </b-button>
36
51
  <b-dropdown-item
37
52
  v-for="(set, index) in setsList"
38
53
  :key="`${set.label_set.id}_${index}`"
@@ -41,23 +56,12 @@
41
56
  >
42
57
  <span>{{
43
58
  `${set.label_set.name} ${
44
- set.id ? numberOfAnnotationSetGroup(set) : `(${$t("new")})`
59
+ set.id
60
+ ? numberOfAnnotationSetGroup(set)
61
+ : `${numberOfLabelSetGroup(set.label_set)} (${$t("new")})`
45
62
  }`
46
63
  }}</span>
47
64
  </b-dropdown-item>
48
- <b-button
49
- type="is-ghost"
50
- :class="[
51
- 'add-ann-set',
52
- 'dropdown-item',
53
- 'no-icon-margin',
54
- setsList.length > 0 ? 'has-border' : '',
55
- ]"
56
- icon-left="plus"
57
- @click="openAnnotationSetCreation"
58
- >
59
- {{ $t("new_ann_set_title") }}
60
- </b-button>
61
65
  </b-dropdown>
62
66
  <b-tooltip
63
67
  multilined
@@ -73,11 +77,11 @@
73
77
  aria-role="list"
74
78
  :disabled="!labelsFiltered || labelsFiltered.length === 0"
75
79
  scrollable
76
- class="label-dropdown annotation-dropdown"
80
+ class="label-dropdown annotation-dropdown dropdown-full-width"
77
81
  >
78
82
  <template #trigger>
79
83
  <b-button
80
- class="popup-input"
84
+ class="popup-input has-right-icon"
81
85
  :disabled="!labelsFiltered"
82
86
  type="is-text"
83
87
  >
@@ -166,6 +170,7 @@ export default {
166
170
  ]),
167
171
  ...mapGetters("document", [
168
172
  "numberOfAnnotationSetGroup",
173
+ "numberOfLabelSetGroup",
169
174
  "labelsFilteredForAnnotationCreation",
170
175
  ]),
171
176
  ...mapGetters("display", ["bboxToRect"]),
@@ -304,12 +309,21 @@ export default {
304
309
  }
305
310
  },
306
311
  chooseLabelSet(labelSet) {
312
+ // check if there's already a new entry for that label set to be created
313
+ const existsIndex = this.setsList.findIndex((set) => {
314
+ return set.id === null && set.label_set.id === labelSet.id;
315
+ });
316
+
307
317
  const newSet = {
308
318
  label_set: labelSet,
309
319
  labels: labelSet.labels,
310
320
  id: null,
311
321
  };
312
- this.setsList.push(newSet);
322
+ if (existsIndex >= 0) {
323
+ this.setsList[existsIndex] = newSet;
324
+ } else {
325
+ this.setsList.unshift(newSet);
326
+ }
313
327
  this.selectedSet = newSet;
314
328
  },
315
329
  openAnnotationSetCreation() {
@@ -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
  {{
@@ -323,7 +332,7 @@ export default {
323
332
  if (existsIndex >= 0) {
324
333
  this.setsList[existsIndex] = newSet;
325
334
  } else {
326
- this.setsList.push(newSet);
335
+ this.setsList.unshift(newSet);
327
336
  }
328
337
  this.selectedSet = newSet;
329
338
  },
@@ -563,9 +563,13 @@ const getters = {
563
563
  let returnLabelSets = [];
564
564
  if (state.annotationSets) {
565
565
  state.annotationSets.forEach((annotationSet) => {
566
+ // last validation checks if the label set is already present in list
566
567
  if (
567
- annotationSet.id == null ||
568
- annotationSet.label_set.has_multiple_annotation_sets
568
+ (annotationSet.id == null ||
569
+ annotationSet.label_set.has_multiple_annotation_sets) &&
570
+ !returnLabelSets.find(
571
+ (set) => set.id !== null && set.id === annotationSet.label_set.id
572
+ )
569
573
  ) {
570
574
  const labelSet = { ...annotationSet.label_set };
571
575
  labelSet.labels = [...annotationSet.labels];
@@ -702,7 +706,10 @@ const getters = {
702
706
  },
703
707
 
704
708
  annotationById: (state) => (annotationId) => {
705
- 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;
706
713
  },
707
714
 
708
715
  // Check if document is ready to be finished
@@ -950,7 +957,7 @@ const actions = {
950
957
  commit("SET_PAGES", []);
951
958
  commit("SET_DOC_ID", id);
952
959
  },
953
- setAnnotationId: ({ commit }, id) => {
960
+ setAnnotationId: ({ commit, dispatch, getters }, id) => {
954
961
  commit("SET_ANNOTATION_ID", id);
955
962
  setURLAnnotationHash(id);
956
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
+ };