@knime/hub-features 1.12.6 → 1.14.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @knime/hub-features
2
2
 
3
+ ## 1.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c75b415: Added SideDrawerHeader component
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [c75b415]
12
+ - @knime/components@1.36.0
13
+
14
+ ## 1.13.0
15
+
16
+ ### Minor Changes
17
+
18
+ - d04922e: display version limit info in ManageVersions side drawer
19
+
3
20
  ## 1.12.6
4
21
 
5
22
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knime/hub-features",
3
- "version": "1.12.6",
3
+ "version": "1.14.0",
4
4
  "description": "Vue components & composables for shared hub features",
5
5
  "homepage": "https://knime.github.io/webapps-common/",
6
6
  "license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
@@ -31,7 +31,7 @@
31
31
  "lodash-es": "4.17.21",
32
32
  "ofetch": "^1.4.1",
33
33
  "typescript": "^5.8.3",
34
- "@knime/components": "1.35.1",
34
+ "@knime/components": "1.36.0",
35
35
  "@knime/styles": "1.11.1",
36
36
  "@knime/utils": "1.5.5"
37
37
  },
@@ -0,0 +1,166 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, ref } from "vue";
3
+ import { useTextareaAutosize } from "@vueuse/core";
4
+
5
+ import {
6
+ Button,
7
+ InputField,
8
+ Label,
9
+ LoadingIcon,
10
+ SideDrawerHeader,
11
+ TextArea,
12
+ } from "@knime/components";
13
+
14
+ type Props = {
15
+ isCreationPending?: boolean;
16
+ };
17
+
18
+ withDefaults(defineProps<Props>(), { isCreationPending: false });
19
+
20
+ const emit = defineEmits<{
21
+ create: [{ name: string; description: string }];
22
+ cancel: [hasUnsavedChanges: boolean];
23
+ }>();
24
+
25
+ const versionName = ref("");
26
+ const versionDescription = ref("");
27
+
28
+ const descriptionEditor = ref<InstanceType<typeof TextArea> | null>(null);
29
+
30
+ onMounted(() => {
31
+ useTextareaAutosize({
32
+ element: descriptionEditor.value?.$el.getElementsByTagName("textarea")[0],
33
+ input: versionDescription,
34
+ });
35
+ });
36
+
37
+ const doValidate = ref(false);
38
+
39
+ const isVersionNameInvalid = computed(() => {
40
+ return doValidate.value && versionName.value.length <= 0;
41
+ });
42
+
43
+ const hasUnsavedChanges = computed(() => {
44
+ return versionName.value !== "" || versionDescription.value !== "";
45
+ });
46
+
47
+ const onCreate = async () => {
48
+ doValidate.value = true;
49
+ await nextTick();
50
+
51
+ if (!isVersionNameInvalid.value) {
52
+ emit("create", {
53
+ name: versionName.value,
54
+ description: versionDescription.value,
55
+ });
56
+ }
57
+ };
58
+ </script>
59
+
60
+ <template>
61
+ <div class="create-version-form">
62
+ <SideDrawerHeader
63
+ title="Create Version"
64
+ description="Give your version a name and description. Versions can be used to create a deployment.
65
+ "
66
+ :is-sub-drawer="true"
67
+ @close="$emit('cancel', hasUnsavedChanges)"
68
+ />
69
+ <div class="form-content">
70
+ <Label text="Version name">
71
+ <InputField
72
+ v-model="versionName"
73
+ type="text"
74
+ required
75
+ :is-valid="!isVersionNameInvalid"
76
+ />
77
+ <div v-if="isVersionNameInvalid" class="error">
78
+ Please enter at least 1 character.
79
+ </div>
80
+ </Label>
81
+ <Label text="Description">
82
+ <TextArea
83
+ ref="descriptionEditor"
84
+ v-model="versionDescription"
85
+ class="description"
86
+ />
87
+ </Label>
88
+ <div class="controls">
89
+ <Button
90
+ with-border
91
+ compact
92
+ class="button cancel"
93
+ @click="$emit('cancel', hasUnsavedChanges)"
94
+ >
95
+ <strong>Cancel</strong>
96
+ </Button>
97
+ <Button
98
+ primary
99
+ compact
100
+ class="button create"
101
+ :disabled="isVersionNameInvalid || isCreationPending"
102
+ @click="onCreate"
103
+ >
104
+ <LoadingIcon v-if="isCreationPending" />
105
+ <strong v-if="!isCreationPending">Create</strong>
106
+ </Button>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </template>
111
+
112
+ <style lang="postcss" scoped>
113
+ .create-version-form {
114
+ height: 100%;
115
+ max-height: 100%;
116
+ width: 100%;
117
+ display: flex;
118
+ flex-direction: column;
119
+
120
+ & .form-content {
121
+ display: flex;
122
+ flex-direction: column;
123
+ height: 100%;
124
+ padding: 30px 30px 0;
125
+ gap: 30px;
126
+ overscroll-behavior: none;
127
+ isolation: isolate;
128
+ overflow: auto;
129
+ z-index: 1;
130
+ background-color: var(--knime-gray-ultra-light);
131
+
132
+ & .description {
133
+ width: 100%;
134
+ max-width: unset;
135
+ display: flex;
136
+
137
+ & :deep(textarea) {
138
+ width: 100%;
139
+ box-sizing: content-box;
140
+ max-height: 200px;
141
+ resize: none;
142
+ }
143
+ }
144
+
145
+ & .error {
146
+ position: absolute;
147
+ word-break: keep-all;
148
+ margin-top: 6px;
149
+ font-weight: 300;
150
+ font-size: 13px;
151
+ color: var(--theme-color-error);
152
+ line-height: 18px;
153
+ }
154
+
155
+ & .controls {
156
+ border-top: 1px solid var(--knime-silver-sand);
157
+ display: flex;
158
+ margin: 0 -30px;
159
+ padding: 10px 20px;
160
+ gap: 10px;
161
+ justify-content: space-between;
162
+ margin-top: auto;
163
+ }
164
+ }
165
+ }
166
+ </style>
@@ -5,16 +5,17 @@ import { Button, LocalDateTime, SubMenu } from "@knime/components";
5
5
  import MenuIcon from "@knime/styles/img/icons/menu-options.svg";
6
6
  import WrenchIcon from "@knime/styles/img/icons/wrench.svg";
7
7
 
8
- import HubAvatar from "../avatars/HubAvatar.vue";
8
+ import HubAvatar from "../../avatars/HubAvatar.vue";
9
+ import type { ItemSavepoint, WithAvatar, WithLabels } from "../types";
9
10
 
10
11
  import LabelList from "./LabelList.vue";
11
- import type { ItemSavepoint, WithAvatar, WithLabels } from "./types";
12
12
 
13
13
  const props = defineProps<{
14
14
  isSelected: boolean;
15
15
  hasEditCapability: boolean;
16
16
  hasPreviousVersion: boolean;
17
17
  currentStateSavepoint: ItemSavepoint & WithAvatar & WithLabels;
18
+ isVersionLimitExceeded?: boolean;
18
19
  }>();
19
20
 
20
21
  const emit = defineEmits<{
@@ -66,7 +67,7 @@ const onMenuItemClick = (_: Event, item: (typeof menuItems.value)[number]) => {
66
67
  </div>
67
68
  <div class="right">
68
69
  <Button
69
- v-if="hasEditCapability"
70
+ v-if="hasEditCapability && !isVersionLimitExceeded"
70
71
  with-border
71
72
  compact
72
73
  class="create-button"
@@ -7,9 +7,8 @@ import { isEqual } from "lodash-es"; // eslint-disable-line depend/ban-dependenc
7
7
  import { FunctionButton } from "@knime/components";
8
8
  import { truncateString } from "@knime/utils";
9
9
 
10
- import Popover from "../Popover.vue";
11
-
12
- import type { AssignedLabel } from "./types";
10
+ import Popover from "../../Popover.vue";
11
+ import type { AssignedLabel } from "../types";
13
12
 
14
13
  type PopoverElement = HTMLElement & {
15
14
  closeMenu: () => void;
@@ -6,15 +6,18 @@ import { FunctionButton } from "@knime/components";
6
6
  import CloseIcon from "@knime/styles/img/icons/close.svg";
7
7
  import HistoryIcon from "@knime/styles/img/icons/history.svg";
8
8
 
9
- import CurrentState from "./CurrentState.vue";
10
- import VersionHistory from "./VersionHistory.vue";
11
- import { CURRENT_STATE_VERSION } from "./constants";
9
+ import { CURRENT_STATE_VERSION } from "../constants";
12
10
  import type {
13
11
  ItemSavepoint,
14
12
  NamedItemVersion,
13
+ VersionLimit,
15
14
  WithAvatar,
16
15
  WithLabels,
17
- } from "./types";
16
+ } from "../types";
17
+
18
+ import CurrentState from "./CurrentState.vue";
19
+ import VersionHistory from "./VersionHistory.vue";
20
+ import VersionLimitInfo from "./VersionLimitInfo.vue";
18
21
 
19
22
  type ManageVersionsProps = {
20
23
  hasUnversionedChanges: boolean;
@@ -25,6 +28,8 @@ type ManageVersionsProps = {
25
28
  hasLoadedAllVersions: boolean;
26
29
  hasAdminRights: boolean;
27
30
  hasEditCapability: boolean;
31
+ versionLimit?: VersionLimit;
32
+ upgradeUrl?: string;
28
33
  };
29
34
 
30
35
  defineProps<ManageVersionsProps>();
@@ -67,6 +72,10 @@ const closeLabelPopovers = throttle(() => {
67
72
  :has-previous-version="versionHistory.length > 0"
68
73
  :is-selected="currentVersion === CURRENT_STATE_VERSION"
69
74
  :current-state-savepoint="unversionedSavepoint"
75
+ :is-version-limit-exceeded="
76
+ versionLimit?.limit !== undefined &&
77
+ versionLimit.currentUsage >= versionLimit.limit
78
+ "
70
79
  @select="$emit('select', CURRENT_STATE_VERSION)"
71
80
  @create-version="$emit('create')"
72
81
  @discard="$emit('discardCurrentState')"
@@ -92,6 +101,15 @@ const closeLabelPopovers = throttle(() => {
92
101
  @load-all="$emit('loadAll')"
93
102
  @select="$emit('select', $event ?? CURRENT_STATE_VERSION)"
94
103
  />
104
+ <VersionLimitInfo
105
+ v-if="versionLimit?.limit !== undefined"
106
+ class="version-limit-info"
107
+ :version-limit="{
108
+ limit: versionLimit.limit,
109
+ currentUsage: versionLimit.currentUsage,
110
+ }"
111
+ :upgrade-url
112
+ />
95
113
  </div>
96
114
  </div>
97
115
  </div>
@@ -155,6 +173,10 @@ const closeLabelPopovers = throttle(() => {
155
173
  & .versions {
156
174
  width: 100%;
157
175
  padding: 30px;
176
+
177
+ & .version-limit-info {
178
+ margin-top: 30px;
179
+ }
158
180
  }
159
181
  }
160
182
 
@@ -1,9 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { IdleReadyButton } from "@knime/components";
3
3
 
4
+ import type { NamedItemVersion, WithAvatar, WithLabels } from "../types";
5
+
4
6
  import NoVersionItem from "./NoVersionItem.vue";
5
7
  import VersionItem from "./VersionItem.vue";
6
- import type { NamedItemVersion, WithAvatar, WithLabels } from "./types";
7
8
 
8
9
  defineProps<{
9
10
  selectedVersion: NamedItemVersion["version"] | null;
@@ -6,10 +6,10 @@ import { LocalDateTime, SubMenu, Tooltip } from "@knime/components";
6
6
  import MenuIcon from "@knime/styles/img/icons/menu-options.svg";
7
7
  import { truncateString } from "@knime/utils";
8
8
 
9
- import HubAvatar from "../avatars/HubAvatar.vue";
9
+ import HubAvatar from "../../avatars/HubAvatar.vue";
10
+ import type { NamedItemVersion, WithAvatar, WithLabels } from "../types";
10
11
 
11
12
  import LabelList from "./LabelList.vue";
12
- import type { NamedItemVersion, WithAvatar, WithLabels } from "./types";
13
13
 
14
14
  const DESCRIPTION_TRUNCATE_LENGTH = 120;
15
15
 
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ import { InlineMessage } from "@knime/components";
5
+
6
+ import type { VersionLimit } from "../types";
7
+
8
+ const { versionLimit, upgradeUrl = undefined } = defineProps<{
9
+ versionLimit: Required<VersionLimit>;
10
+ upgradeUrl?: string;
11
+ }>();
12
+
13
+ const isLimitReached = computed(
14
+ () => versionLimit.currentUsage >= versionLimit.limit,
15
+ );
16
+
17
+ const title = computed(() => {
18
+ if (isLimitReached.value) {
19
+ return `You've reached the maximum of ${versionLimit.limit} versions.`;
20
+ } else {
21
+ return `Using ${versionLimit.currentUsage} of ${versionLimit.limit} versions.`;
22
+ }
23
+ });
24
+ </script>
25
+
26
+ <template>
27
+ <InlineMessage variant="info" :title>
28
+ <template v-if="upgradeUrl"><a :href="upgradeUrl">Upgrade</a></template>
29
+ <template v-else>Upgrade</template>
30
+ to use unlimited versions.
31
+ </InlineMessage>
32
+ </template>
@@ -8,11 +8,12 @@ import type {
8
8
  ItemSavepoint,
9
9
  NamedItemVersion,
10
10
  RepositoryItem,
11
+ VersionLimit,
11
12
  WithAvatar,
12
13
  WithLabels,
13
14
  } from "@knime/hub-features/versions";
14
15
 
15
- import { getFetchClient } from "../../common/ofetchClient";
16
+ import { getFetchClient } from "../../../common/ofetchClient";
16
17
 
17
18
  type UseVersionsApiOptions = {
18
19
  customFetchClientOptions?: FetchOptions;
@@ -51,6 +52,12 @@ export const useVersionsApi = ({
51
52
  return doHubRequest(`/repository/${itemId}`) as Promise<RepositoryItem>;
52
53
  };
53
54
 
55
+ const fetchVersionLimit = ({ itemId }: { itemId: string }) => {
56
+ return doHubRequest(
57
+ `/repository/limits/${itemId}/item-versions`,
58
+ ) as Promise<VersionLimit>;
59
+ };
60
+
54
61
  const fetchVersions = ({
55
62
  itemId,
56
63
  loadAll,
@@ -214,6 +221,7 @@ export const useVersionsApi = ({
214
221
 
215
222
  return {
216
223
  fetchVersions,
224
+ fetchVersionLimit,
217
225
  fetchResourceLabels,
218
226
  fetchItemSavepoints,
219
227
  fetchPermissions,
@@ -1,7 +1,7 @@
1
- import CreateVersionForm from "./CreateVersionForm.vue";
2
- import ManageVersions from "./ManageVersions.vue";
1
+ import CreateVersionForm from "./components/CreateVersionForm.vue";
2
+ import ManageVersions from "./components/ManageVersions.vue";
3
3
 
4
4
  export { CreateVersionForm, ManageVersions };
5
5
  export type * from "./types";
6
6
  export * from "./constants";
7
- export * from "./useVersionsApi";
7
+ export * from "./composables/useVersionsApi";
@@ -70,3 +70,8 @@ export type ItemPermission =
70
70
  | "CONFIGURATION"
71
71
  | "MOVE"
72
72
  | "COPY";
73
+
74
+ export type VersionLimit = {
75
+ currentUsage: number;
76
+ limit?: number;
77
+ };
@@ -1,150 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, nextTick, onMounted, ref } from "vue";
3
- import { useTextareaAutosize } from "@vueuse/core";
4
-
5
- import {
6
- Button,
7
- InputField,
8
- Label,
9
- LoadingIcon,
10
- TextArea,
11
- } from "@knime/components";
12
-
13
- type Props = {
14
- isCreationPending?: boolean;
15
- };
16
-
17
- withDefaults(defineProps<Props>(), { isCreationPending: false });
18
-
19
- const emit = defineEmits<{
20
- create: [{ name: string; description: string }];
21
- cancel: [hasUnsavedChanges: boolean];
22
- }>();
23
-
24
- const versionName = ref("");
25
- const versionDescription = ref("");
26
-
27
- const descriptionEditor = ref<InstanceType<typeof TextArea> | null>(null);
28
-
29
- onMounted(() => {
30
- useTextareaAutosize({
31
- element: descriptionEditor.value?.$el.getElementsByTagName("textarea")[0],
32
- input: versionDescription,
33
- });
34
- });
35
-
36
- const doValidate = ref(false);
37
-
38
- const isVersionNameInvalid = computed(() => {
39
- return versionName.value.length <= 0 && doValidate.value;
40
- });
41
-
42
- const hasUnsavedChanges = computed(() => {
43
- return versionName.value !== "" || versionDescription.value !== "";
44
- });
45
-
46
- const onCreate = async () => {
47
- doValidate.value = true;
48
- await nextTick();
49
-
50
- if (!isVersionNameInvalid.value) {
51
- emit("create", {
52
- name: versionName.value,
53
- description: versionDescription.value,
54
- });
55
- }
56
- };
57
- </script>
58
-
59
- <template>
60
- <div class="create-version-form">
61
- <Label text="Version name">
62
- <InputField
63
- v-model="versionName"
64
- type="text"
65
- required
66
- :is-valid="!isVersionNameInvalid"
67
- />
68
- <div v-if="isVersionNameInvalid" class="error">
69
- Please enter at least 1 character.
70
- </div>
71
- </Label>
72
- <Label text="Description">
73
- <TextArea
74
- ref="descriptionEditor"
75
- v-model="versionDescription"
76
- class="description"
77
- />
78
- </Label>
79
- <div class="controls">
80
- <Button
81
- with-border
82
- compact
83
- class="button cancel"
84
- @click="$emit('cancel', hasUnsavedChanges)"
85
- >
86
- <strong>Cancel</strong>
87
- </Button>
88
- <Button
89
- primary
90
- compact
91
- class="button create"
92
- :disabled="isVersionNameInvalid || isCreationPending"
93
- @click="onCreate"
94
- >
95
- <LoadingIcon v-if="isCreationPending" />
96
- <strong v-if="!isCreationPending">Create</strong>
97
- </Button>
98
- </div>
99
- </div>
100
- </template>
101
-
102
- <style lang="postcss" scoped>
103
- .create-version-form {
104
- height: 100%;
105
- max-height: 100%;
106
- width: 100%;
107
- display: flex;
108
- flex-direction: column;
109
- padding: 30px 30px 0;
110
- gap: 30px;
111
- overscroll-behavior: none;
112
- isolation: isolate;
113
- overflow: auto;
114
- z-index: 1;
115
- background-color: var(--knime-gray-ultra-light);
116
-
117
- & .description {
118
- width: 100%;
119
- max-width: unset;
120
- display: flex;
121
-
122
- & :deep(textarea) {
123
- width: 100%;
124
- box-sizing: content-box;
125
- max-height: 200px;
126
- resize: none;
127
- }
128
- }
129
-
130
- & .error {
131
- position: absolute;
132
- word-break: keep-all;
133
- margin-top: 6px;
134
- font-weight: 300;
135
- font-size: 13px;
136
- color: var(--theme-color-error);
137
- line-height: 18px;
138
- }
139
-
140
- & .controls {
141
- border-top: 1px solid var(--knime-silver-sand);
142
- display: flex;
143
- margin: 0 -30px;
144
- padding: 10px 20px;
145
- gap: 10px;
146
- justify-content: space-between;
147
- margin-top: auto;
148
- }
149
- }
150
- </style>