@knime/hub-features 1.2.24 → 1.3.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.
@@ -0,0 +1,256 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ import { Button, LocalDateTime, SubMenu } from "@knime/components";
5
+ import MenuIcon from "@knime/styles/img/icons/menu-options.svg";
6
+ import WrenchIcon from "@knime/styles/img/icons/wrench.svg";
7
+
8
+ import HubAvatar from "../avatars/HubAvatar.vue";
9
+
10
+ import LabelList from "./LabelList.vue";
11
+ import type { ItemSavepoint, WithAvatar, WithLabels } from "./types";
12
+
13
+ const props = defineProps<{
14
+ isSelected: boolean;
15
+ hasEditCapability: boolean;
16
+ currentStateSavepoint: ItemSavepoint & WithAvatar & WithLabels;
17
+ }>();
18
+
19
+ const emit = defineEmits<{
20
+ createVersion: [];
21
+ discard: [];
22
+ select: [];
23
+ }>();
24
+
25
+ const menuItems = computed(() => {
26
+ return props.hasEditCapability
27
+ ? [
28
+ {
29
+ text: "Discard",
30
+ action: () => emit("discard"),
31
+ },
32
+ ]
33
+ : [];
34
+ });
35
+
36
+ const onMenuItemClick = (_: Event, item: (typeof menuItems.value)[number]) => {
37
+ item.action();
38
+ };
39
+ </script>
40
+
41
+ <template>
42
+ <div class="wrapper">
43
+ <div
44
+ class="current-state"
45
+ :class="{ selected: isSelected }"
46
+ @click="!isSelected && $emit('select')"
47
+ >
48
+ <div class="left">
49
+ <div class="draft">
50
+ <h6><WrenchIcon class="draft-icon" /><span>Draft</span></h6>
51
+
52
+ <div class="labels">
53
+ <LabelList :labels="currentStateSavepoint.labels" />
54
+ </div>
55
+
56
+ <span class="date">
57
+ Latest edits on
58
+ <LocalDateTime
59
+ class="date"
60
+ show-time
61
+ :date="currentStateSavepoint.changes[0].createdOn"
62
+ />
63
+ </span>
64
+ </div>
65
+ </div>
66
+ <div class="right">
67
+ <Button
68
+ v-if="hasEditCapability"
69
+ with-border
70
+ compact
71
+ class="create-button"
72
+ @click="$emit('createVersion')"
73
+ >
74
+ Create version
75
+ </Button>
76
+ <HubAvatar
77
+ class="profile-picture"
78
+ v-bind="currentStateSavepoint.avatar"
79
+ />
80
+ <div v-if="menuItems.length" class="controls">
81
+ <nav>
82
+ <SubMenu
83
+ :items="menuItems"
84
+ button-title="Options"
85
+ @item-click="onMenuItemClick"
86
+ >
87
+ <MenuIcon class="open-icon" />
88
+ </SubMenu>
89
+ </nav>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <style lang="postcss" scoped>
97
+ @import url("@knime/styles/css/mixins.css");
98
+
99
+ .wrapper {
100
+ position: relative;
101
+ margin-left: 30px;
102
+
103
+ &::before {
104
+ content: "";
105
+ position: absolute;
106
+ left: -22px;
107
+ top: 0;
108
+ width: 2px;
109
+ height: calc(100% + 30px);
110
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 5 5'%3E%3Ccircle cx='1' cy='1' r='.5' fill='%236E6E6E'/%3E%3C/svg%3E");
111
+ background-size: 5px;
112
+ background-repeat: repeat-y;
113
+ }
114
+
115
+ & .current-state {
116
+ background-color: var(--knime-gray-ultra-light);
117
+ display: flex;
118
+ justify-content: space-between;
119
+ align-items: center;
120
+ padding: 10px;
121
+ gap: 10px;
122
+ transition: background-color 0.25s ease;
123
+
124
+ & .left {
125
+ & .draft {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 3px;
129
+ overflow-wrap: anywhere;
130
+ word-break: break-word;
131
+
132
+ & h6 {
133
+ display: flex;
134
+ align-items: center;
135
+ margin: 0;
136
+ font-size: 13px;
137
+ line-height: 18px;
138
+ font-weight: 700;
139
+ transition: color 0.25s ease;
140
+
141
+ & .draft-icon {
142
+ @mixin svg-icon-size 20;
143
+
144
+ margin-right: 4px;
145
+ background: var(--knime-carrot-light);
146
+ padding: 4px;
147
+ border-radius: 1000px;
148
+ }
149
+ }
150
+
151
+ & .labels {
152
+ display: flex;
153
+ flex-flow: row wrap;
154
+ align-items: center;
155
+ gap: 5px;
156
+ }
157
+
158
+ & .date {
159
+ margin: 0;
160
+ font-size: 11px;
161
+ line-height: 16px;
162
+ font-weight: 300;
163
+ transition: color 0.25s ease;
164
+ }
165
+ }
166
+ }
167
+
168
+ & .right {
169
+ display: flex;
170
+ justify-content: flex-end;
171
+ align-items: center;
172
+ gap: 8px;
173
+
174
+ & .create-button {
175
+ white-space: nowrap;
176
+ }
177
+
178
+ & .controls {
179
+ display: flex;
180
+ justify-content: center;
181
+ flex-direction: column;
182
+ border-left: 1px solid var(--knime-silver-sand);
183
+ padding-left: 5px;
184
+ margin-right: -4px;
185
+ }
186
+ }
187
+
188
+ &::before {
189
+ content: "";
190
+ width: 12px;
191
+ height: 12px;
192
+ background-color: var(--knime-gray-ultra-light);
193
+ transform: rotateZ(45deg);
194
+ position: absolute;
195
+ left: -6px;
196
+ top: 12px;
197
+ transition: background-color 0.25s ease;
198
+ }
199
+
200
+ &::after {
201
+ content: "";
202
+ position: absolute;
203
+ left: -24px;
204
+ top: 15px;
205
+ background-color: var(--knime-masala);
206
+ width: 6px;
207
+ height: 6px;
208
+ border-radius: 100%;
209
+ box-shadow: 0 0 0 0 transparent;
210
+ transition:
211
+ background-color 0.25s ease,
212
+ box-shadow 0.25s ease;
213
+ }
214
+
215
+ &:hover {
216
+ box-shadow: 0 2px 10px 0 var(--knime-gray-dark-semi);
217
+ cursor: pointer;
218
+ }
219
+
220
+ &.selected {
221
+ background-color: var(--knime-cornflower-semi);
222
+
223
+ & .left {
224
+ & h6,
225
+ & .date {
226
+ color: var(--knime-cornflower-dark);
227
+
228
+ & .draft-icon {
229
+ background: none;
230
+ stroke: var(--knime-cornflower-dark);
231
+ }
232
+ }
233
+ }
234
+
235
+ & .controls {
236
+ border-left: 1px solid var(--knime-cornflower-light);
237
+ }
238
+
239
+ &::before {
240
+ background-color: var(--knime-cornflower-semi);
241
+ }
242
+
243
+ &::after {
244
+ background-color: var(--knime-cornflower);
245
+ box-shadow:
246
+ 0 0 0 3px var(--knime-porcelain),
247
+ 0 0 0 5px var(--knime-cornflower);
248
+ }
249
+ }
250
+
251
+ @media only screen and (width <= 900px) {
252
+ width: calc(100vw - 100px);
253
+ }
254
+ }
255
+ }
256
+ </style>
@@ -0,0 +1,254 @@
1
+ <script setup lang="ts">
2
+ import { type Ref, computed, onMounted, ref } from "vue";
3
+ import { useEventBus } from "@vueuse/core";
4
+ import { autoUpdate, offset, useFloating } from "@floating-ui/vue";
5
+ import { isEqual } from "lodash-es";
6
+
7
+ import { FunctionButton } from "@knime/components";
8
+ import { truncateString } from "@knime/utils";
9
+
10
+ import Popover from "../Popover.vue";
11
+
12
+ import type { AssignedLabel } from "./types";
13
+
14
+ interface PopoverElement extends HTMLElement {
15
+ closeMenu: () => void;
16
+ openMenu: () => void;
17
+ }
18
+
19
+ const LABEL_LENGTH = 25;
20
+
21
+ const props = withDefaults(
22
+ defineProps<{
23
+ labels: Array<AssignedLabel>;
24
+ defaultLabelCount?: number;
25
+ }>(),
26
+ { defaultLabelCount: 3 },
27
+ );
28
+ const emit = defineEmits(["labelOver", "labelLeave"]);
29
+
30
+ const eventBus = useEventBus("versionLabels");
31
+ const eventBusKey = "versionLabelShowPopover";
32
+
33
+ const showAll = ref(false);
34
+ const activeLabel: Ref<AssignedLabel | null> = ref(null);
35
+
36
+ const popover: Ref<PopoverElement | null> = ref(null);
37
+ const floatingPanel: Ref<HTMLElement | null> = ref(null);
38
+ const floatingAnchor: Ref<HTMLElement | null> = ref(null);
39
+ // 40px is the offset of the arrow on the popover plus the top padding
40
+ // this is necessary to compensate for the offset calculation
41
+ const defaultCrossAxisOffset = -46;
42
+ const defaultMainAxisOffset = 75;
43
+
44
+ const createMiddleware = () => [
45
+ offset({
46
+ crossAxis: defaultCrossAxisOffset,
47
+ mainAxis: defaultMainAxisOffset,
48
+ }),
49
+ ];
50
+
51
+ const { floatingStyles } = useFloating(floatingAnchor, floatingPanel, {
52
+ whileElementsMounted: autoUpdate,
53
+ placement: "left-start",
54
+ strategy: "fixed",
55
+ middleware: createMiddleware(),
56
+ });
57
+
58
+ const popoverTopOffset = ref(0);
59
+ const popoverFloatingStyles = computed(() => {
60
+ const parsedTop = parseInt(floatingStyles.value.top.replace("px", ""), 10);
61
+ const compensatedTop = `${parsedTop + popoverTopOffset.value}px`;
62
+
63
+ return {
64
+ ...floatingStyles.value,
65
+ top: compensatedTop,
66
+ };
67
+ });
68
+
69
+ const isShowMoreVisible = computed(
70
+ () => props.labels.length > props.defaultLabelCount && !showAll.value,
71
+ );
72
+
73
+ const showMoreButtonText = computed(
74
+ () => `+${props.labels.length - props.defaultLabelCount}`,
75
+ );
76
+
77
+ const filteredLabels = computed(() =>
78
+ showAll.value ? props.labels : props.labels.slice(0, props.defaultLabelCount),
79
+ );
80
+
81
+ const calculateDistanceToUpperBorder = (labelElement: HTMLElement) => {
82
+ const labelRect = labelElement.getBoundingClientRect();
83
+ const floatingAnchorRect = floatingAnchor.value!.getBoundingClientRect();
84
+
85
+ return Math.round(labelRect.top - floatingAnchorRect.top);
86
+ };
87
+
88
+ const isActiveLabel = (label: AssignedLabel) =>
89
+ isEqual(activeLabel.value, label);
90
+
91
+ const togglePopover = (label: AssignedLabel, labelElement: HTMLElement) => {
92
+ if (isActiveLabel(label)) {
93
+ activeLabel.value = null;
94
+ popover.value?.closeMenu();
95
+ } else {
96
+ eventBus.emit(eventBusKey);
97
+ popoverTopOffset.value = calculateDistanceToUpperBorder(labelElement);
98
+
99
+ activeLabel.value = label;
100
+ popover.value?.openMenu();
101
+ }
102
+ };
103
+
104
+ const showMore = () => {
105
+ showAll.value = !showAll.value;
106
+ };
107
+
108
+ const mouseOver = () => emit("labelOver");
109
+ const mouseLeave = () => emit("labelLeave");
110
+
111
+ onMounted(() => {
112
+ eventBus.on((event: unknown) => {
113
+ if (event === eventBusKey) {
114
+ popover.value?.closeMenu();
115
+ activeLabel.value = null;
116
+ }
117
+ });
118
+ });
119
+ </script>
120
+
121
+ <template>
122
+ <template v-if="labels.length > 0">
123
+ <div class="label-list-with-popover">
124
+ <div ref="floatingAnchor" class="label-list">
125
+ <FunctionButton
126
+ v-for="label in filteredLabels"
127
+ :key="label.labelId"
128
+ compact
129
+ class="with-border"
130
+ :active="isActiveLabel(label)"
131
+ @mouseover="mouseOver"
132
+ @mouseleave="mouseLeave"
133
+ @click.stop="togglePopover(label, $event.currentTarget)"
134
+ @keydown.esc.stop="popover?.closeMenu()"
135
+ >
136
+ {{ truncateString(label.label.name, LABEL_LENGTH) }}
137
+ </FunctionButton>
138
+
139
+ <FunctionButton
140
+ v-if="isShowMoreVisible"
141
+ compact
142
+ class="with-border"
143
+ @mouseover="mouseOver"
144
+ @mouseleave="mouseLeave"
145
+ @click.stop="showMore"
146
+ >{{ showMoreButtonText }}</FunctionButton
147
+ >
148
+ </div>
149
+
150
+ <div
151
+ ref="floatingPanel"
152
+ class="floating-panel"
153
+ :style="popoverFloatingStyles"
154
+ @click.stop
155
+ @mouseover="mouseOver"
156
+ @mouseleave="mouseLeave"
157
+ >
158
+ <Popover
159
+ ref="popover"
160
+ :use-button="false"
161
+ :button-with-border="false"
162
+ arrow-position="right"
163
+ @close="activeLabel = null"
164
+ >
165
+ <template #content>
166
+ <h6 class="headline">
167
+ {{ activeLabel?.label.name }}
168
+ </h6>
169
+ <div class="panel">
170
+ <div class="description">
171
+ {{ activeLabel?.label.description }}
172
+ </div>
173
+ <div class="message">{{ activeLabel?.message }}</div>
174
+ </div>
175
+ </template>
176
+ </Popover>
177
+ </div>
178
+ </div>
179
+ </template>
180
+ </template>
181
+
182
+ <style lang="postcss" scoped>
183
+ .label-list-with-popover {
184
+ position: relative;
185
+
186
+ & .label-list {
187
+ position: relative;
188
+ display: flex;
189
+ flex-flow: row wrap;
190
+ align-items: center;
191
+ gap: var(--space-4);
192
+ padding: var(--space-6) 0;
193
+
194
+ & .function-button {
195
+ &.with-border {
196
+ padding: 2px var(--space-8);
197
+ border: 1px solid var(--knime-silver-sand);
198
+
199
+ &.active {
200
+ border-color: var(--theme-button-function-background-color-active);
201
+ }
202
+ }
203
+ }
204
+
205
+ &:empty {
206
+ display: none;
207
+ }
208
+ }
209
+
210
+ & .floating-panel {
211
+ z-index: 3;
212
+ }
213
+ }
214
+
215
+ /* the position is set back because it interferes with the position calculation of floating UI */
216
+ .popover.expanded :deep(.content) {
217
+ position: initial;
218
+ }
219
+
220
+ & .headline,
221
+ & .panel {
222
+ line-height: 1.5;
223
+ }
224
+
225
+ & .headline {
226
+ font-size: 16px;
227
+ font-weight: 700;
228
+ margin: 0;
229
+ margin: 0 0 var(--space-8);
230
+ padding-right: 18px;
231
+ }
232
+
233
+ & .panel {
234
+ & .description {
235
+ margin-bottom: var(--space-6);
236
+ }
237
+
238
+ & .message,
239
+ & .description {
240
+ font-weight: 300;
241
+ font-size: 13px;
242
+
243
+ &:empty {
244
+ display: none;
245
+ }
246
+ }
247
+ }
248
+
249
+ @media only screen and (width <= 900px) {
250
+ .label-list-with-popover {
251
+ display: none;
252
+ }
253
+ }
254
+ </style>
@@ -0,0 +1,165 @@
1
+ <script setup lang="ts">
2
+ import { useEventBus } from "@vueuse/core";
3
+ import { throttle } from "lodash-es";
4
+
5
+ import { FunctionButton } from "@knime/components";
6
+ import CloseIcon from "@knime/styles/img/icons/close.svg";
7
+ import HistoryIcon from "@knime/styles/img/icons/history.svg";
8
+
9
+ import CurrentState from "./CurrentState.vue";
10
+ import VersionHistory from "./VersionHistory.vue";
11
+ import { CURRENT_STATE_VERSION } from "./constants";
12
+ import type {
13
+ ItemSavepoint,
14
+ NamedItemVersion,
15
+ WithAvatar,
16
+ WithLabels,
17
+ } from "./types";
18
+
19
+ type ManageVersionsProps = {
20
+ hasUnversionedChanges: boolean;
21
+ unversionedSavepoint?: ItemSavepoint & WithAvatar & WithLabels;
22
+ currentVersion: NamedItemVersion["version"] | null;
23
+ versionHistory: Array<NamedItemVersion & WithAvatar & WithLabels>;
24
+ loading: boolean;
25
+ hasLoadedAllVersions: boolean;
26
+ hasAdminRights: boolean;
27
+ hasEditCapability: boolean;
28
+ };
29
+
30
+ defineProps<ManageVersionsProps>();
31
+
32
+ defineEmits<{
33
+ close: [];
34
+ loadAll: [];
35
+ create: [];
36
+ delete: [version: NamedItemVersion["version"]];
37
+ restore: [version: NamedItemVersion["version"]];
38
+ select: [version: NamedItemVersion["version"]];
39
+ }>();
40
+
41
+ const labelsEventBus = useEventBus("versionLabels");
42
+
43
+ const closeLabelPopovers = throttle(() => {
44
+ labelsEventBus.emit("versionLabelShowPopover");
45
+ // eslint-disable-next-line no-magic-numbers
46
+ }, 10000); // Arbitrary delay to reduce overhead, is automatically reset @scrollend
47
+ </script>
48
+
49
+ <template>
50
+ <div class="manage-versions-container">
51
+ <FunctionButton class="close" @click="$emit('close')">
52
+ <CloseIcon />
53
+ </FunctionButton>
54
+
55
+ <div class="manage-versions">
56
+ <div class="header">
57
+ <HistoryIcon class="history-icon" /><!--
58
+ -->
59
+ <h4>Version history</h4>
60
+ <div
61
+ v-if="hasUnversionedChanges && unversionedSavepoint"
62
+ class="changes"
63
+ >
64
+ <CurrentState
65
+ :has-edit-capability
66
+ :is-selected="currentVersion === CURRENT_STATE_VERSION"
67
+ :current-state-savepoint="unversionedSavepoint"
68
+ @select="$emit('select', CURRENT_STATE_VERSION)"
69
+ @create-version="$emit('create')"
70
+ />
71
+ </div>
72
+ </div>
73
+ <div
74
+ class="overflow-container"
75
+ @scroll="closeLabelPopovers"
76
+ @scrollend="closeLabelPopovers.cancel"
77
+ >
78
+ <div class="versions">
79
+ <VersionHistory
80
+ :selected-version="currentVersion"
81
+ :version-history
82
+ :loading
83
+ :has-unversioned-changes
84
+ :has-loaded-all-versions
85
+ :has-admin-rights
86
+ :has-edit-capability
87
+ @delete="$emit('delete', $event)"
88
+ @restore="$emit('restore', $event)"
89
+ @load-all="$emit('loadAll')"
90
+ @select="$emit('select', $event ?? CURRENT_STATE_VERSION)"
91
+ />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </template>
97
+
98
+ <style lang="postcss" scoped>
99
+ @import url("@knime/styles/css/mixins.css");
100
+
101
+ .manage-versions-container {
102
+ position: relative;
103
+ background-color: var(--knime-gray-light-semi);
104
+ height: 100%;
105
+ display: flex;
106
+ flex-direction: column;
107
+ justify-content: flex-start;
108
+ align-items: flex-start;
109
+ overscroll-behavior: none;
110
+ isolation: isolate;
111
+
112
+ & .close {
113
+ position: absolute;
114
+ top: 6px;
115
+ right: 6px;
116
+ z-index: 3;
117
+ }
118
+
119
+ & .manage-versions {
120
+ display: flex;
121
+ flex-direction: column;
122
+ width: 100%;
123
+ height: 100%;
124
+
125
+ & .header {
126
+ background-color: var(--knime-white);
127
+ z-index: 2;
128
+ box-shadow: var(--shadow-elevation-1);
129
+ padding: 32px 30px 30px;
130
+
131
+ & h4 {
132
+ display: inline;
133
+ margin: 0;
134
+ font-size: 22px;
135
+ line-height: 26px;
136
+ }
137
+
138
+ & .history-icon {
139
+ @mixin svg-icon-size 24;
140
+
141
+ margin-right: 9px;
142
+ vertical-align: sub;
143
+ stroke: var(--knime-masala);
144
+ position: relative;
145
+ top: 1px;
146
+ }
147
+ }
148
+
149
+ & .overflow-container {
150
+ overflow-y: auto;
151
+
152
+ & .versions {
153
+ width: 100%;
154
+ padding: 30px;
155
+ }
156
+ }
157
+
158
+ & .changes {
159
+ background-color: var(--knime-white);
160
+ width: 100%;
161
+ padding-top: 30px;
162
+ }
163
+ }
164
+ }
165
+ </style>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div class="no-versions-item">No version created yet</div>
3
+ </template>
4
+
5
+ <style lang="postcss" scoped>
6
+ .no-versions-item {
7
+ font-weight: 300;
8
+ font-size: 12px;
9
+ line-height: 15px;
10
+ padding-top: 4px;
11
+
12
+ &::after {
13
+ content: "";
14
+ position: absolute;
15
+ left: -4px;
16
+ top: 11px;
17
+ background-color: var(--knime-masala);
18
+ width: 6px;
19
+ height: 6px;
20
+ border-radius: 100%;
21
+ }
22
+ }
23
+ </style>