Skip to content

Commit 4c5b09b

Browse files
Merge branch 'develop' into 'fb-optic-2155/info-tab-is-not-optimized-for-dark-mode'
Workflow run: https://github.com/HumanSignal/label-studio/actions/runs/14932344278
2 parents 8b87765 + 524f640 commit 4c5b09b

File tree

22 files changed

+225
-97
lines changed

22 files changed

+225
-97
lines changed

label_studio/io_storages/README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,29 @@ The Storage Proxy API behavior can be configured using the following environment
184184
| `RESOLVER_PROXY_MAX_RANGE_SIZE` | Maximum size in bytes for a single range request | 7*1024*1024 |
185185
| `RESOLVER_PROXY_CACHE_TIMEOUT` | Cache TTL in seconds for proxy responses | 3600 |
186186

187-
These optimizations ensure that the Proxy API remains responsive and resource-efficient, even when handling large files or many concurrent requests.
187+
These optimizations ensure that the Proxy API remains responsive and resource-efficient, even when handling large files or many concurrent requests.
188+
189+
## Multiple Storages and URL Resolving
190+
191+
There are use cases where multiple storages can/must be used in a single project. This can cause some confusion as to which storage gets used when. Here are some common cases and how to set up mutliple storages properly.
192+
193+
### Case 1 - Tasks Referencing Other Buckets
194+
* bucket-A containing JSON tasks
195+
* bucket-B containing images/text/other data
196+
* Tasks synced from bucket-A have references to data in bucket-B
197+
198+
##### How To Setup
199+
* Add storage 1 for bucket-A
200+
* Add storage 2 for bucket-B (might be same or different credentials than bucket-A)
201+
* Sync storage 1
202+
* All references to data in bucket-B will be resolved using storage 2 automatically
203+
204+
### Case 2 - Buckets with Different Credentials
205+
* bucket-A accessible by credentials 1
206+
* bucket-B accessible by credentials 2
207+
208+
##### How To Setup
209+
* Add storage 1 for bucket-A with credentials 1
210+
* Add storage 2 for bucket-B with credentials 2
211+
* Sync both storages
212+
* The appropriate storage will be used to resolve urls/generate presigned URLs

label_studio/io_storages/base_models.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from django.utils import timezone
2828
from django.utils.translation import gettext_lazy as _
2929
from django_rq import job
30-
from io_storages.utils import get_uri_via_regex
30+
from io_storages.utils import get_uri_via_regex, parse_bucket_uri
3131
from rq.job import Job
3232
from tasks.models import Annotation, Task
3333
from tasks.serializers import AnnotationSerializer, PredictionSerializer
@@ -255,8 +255,19 @@ def can_resolve_scheme(self, url: Union[str, None]) -> bool:
255255
return False
256256
# TODO: Search for occurrences inside string, e.g. for cases like "gs://bucket/file.pdf" or "<embed src='gs://bucket/file.pdf'/>"
257257
_, prefix = get_uri_via_regex(url, prefixes=(self.url_scheme,))
258-
if prefix == self.url_scheme:
259-
return True
258+
bucket_uri = parse_bucket_uri(url, self)
259+
260+
# If there is a prefix and the bucket matches the storage's bucket/container/path
261+
if prefix == self.url_scheme and bucket_uri:
262+
# bucket is used for s3 and gcs
263+
if hasattr(self, 'bucket') and bucket_uri.bucket == self.bucket:
264+
return True
265+
# container is used for azure blob
266+
if hasattr(self, 'container') and bucket_uri.bucket == self.container:
267+
return True
268+
# path is used for redis
269+
if hasattr(self, 'path') and bucket_uri.bucket == self.path:
270+
return True
260271
# if not found any occurrences - this Storage can't resolve url
261272
return False
262273

label_studio/io_storages/functions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,5 @@ def get_storage_by_url(url: Union[str, List, Dict], storage_objects: Iterable[Im
5454
for storage_object in storage_objects:
5555
if storage_object.can_resolve_url(url):
5656
# note: only first found storage_object will be used for link resolving
57-
# probably we need to use more advanced can_resolve_url mechanics
58-
# that takes into account not only prefixes, but bucket path too
57+
# can_resolve_url now checks both the scheme and the bucket to ensure the correct storage is used
5958
return storage_object

label_studio/io_storages/s3/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def validate(self, data):
6868
except TypeError as e:
6969
logger.info(f'It seems access keys are incorrect: {e}', exc_info=True)
7070
raise ValidationError('It seems access keys are incorrect')
71+
except KeyError:
72+
raise ValidationError(f'{storage.url_scheme}://{storage.bucket}/{storage.prefix} not found.')
7173
return data
7274

7375

label_studio/tasks/models.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,10 @@ def prepare_filename(filename):
420420
def resolve_storage_uri(self, url) -> Optional[Mapping[str, Any]]:
421421
from io_storages.functions import get_storage_by_url
422422

423-
storage = self.storage
424-
project = self.project
425-
426-
if not storage:
427-
storage_objects = project.get_all_import_storage_objects
428-
storage = get_storage_by_url(url, storage_objects)
423+
# Instead of using self.storage, we check all storage objects for the project to
424+
# support imported tasks that point to another bucket
425+
storage_objects = self.project.get_all_import_storage_objects
426+
storage = get_storage_by_url(url, storage_objects)
429427

430428
if storage:
431429
return {
@@ -468,10 +466,9 @@ def resolve_uri(self, task_data, project):
468466

469467
# project storage
470468
# TODO: to resolve nested lists and dicts we should improve get_storage_by_url(),
471-
# TODO: problem with current approach: it can be used only the first storage that get_storage_by_url
472-
# TODO: returns. However, maybe the second storage will resolve uris properly.
473-
# TODO: resolve_uri() already supports them
474-
storage = self.storage or get_storage_by_url(task_data[field], storage_objects)
469+
# Now always using get_storage_by_url to ensure the storage with the correct bucket is used
470+
# As a last fallback we can use self.storage which is the storage the Task was imported from
471+
storage = get_storage_by_url(task_data[field], storage_objects) or self.storage
475472
if storage:
476473
try:
477474
resolved_uri = storage.resolve_uri(task_data[field], self)

label_studio/tests/export.tavern.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@ stages:
139139
tags:
140140
- image segmentation
141141
- object detection
142+
- keypoints
142143
name: COCO
143144
- title: COCO with Images
144145
description: COCO format with images downloaded.
145146
link: https://labelstud.io/guide/export.html#COCO
146147
tags:
147148
- image segmentation
148149
- object detection
150+
- keypoints
149151
name: COCO_WITH_IMAGES
150152
- title: YOLO
151153
description: Popular TXT format is created for each image file. Each txt file contains
@@ -155,13 +157,15 @@ stages:
155157
tags:
156158
- image segmentation
157159
- object detection
160+
- keypoints
158161
name: YOLO
159162
- title: YOLO with Images
160163
description: YOLO format with images downloaded.
161164
link: https://labelstud.io/guide/export.html#YOLO
162165
tags:
163166
- image segmentation
164167
- object detection
168+
- keypoints
165169
name: YOLO_WITH_IMAGES
166170
- title: YOLOv8 OBB
167171
description: Popular TXT format is created for each image file. Each txt file contains

poetry.lock

Lines changed: 33 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ dependencies = [
7373
"djangorestframework-simplejwt[crypto] (>=5.4.0,<6.0.0)",
7474
"tldextract (>=5.1.3)",
7575
## HumanSignal repo dependencies :start
76-
"label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/1ef88e2f8afe50a738fc1b03385cb1f3d6b2dd9f.zip",
76+
"label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/c4ed0b0240c4a5e9f372393f84fdea01abe33141.zip",
7777
## HumanSignal repo dependencies :end
7878
]
7979

web/libs/datamanager/src/components/App/App.jsx renamed to web/libs/datamanager/src/components/App/App.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,47 @@ import { DataManager } from "../DataManager/DataManager";
88
import { Labeling } from "../Label/Label";
99
import "./App.scss";
1010

11-
class ErrorBoundary extends React.Component {
12-
state = {
11+
interface ErrorBoundaryProps {
12+
children: React.ReactNode;
13+
}
14+
15+
interface ErrorBoundaryState {
16+
error: Error | null;
17+
}
18+
19+
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
20+
state: ErrorBoundaryState = {
1321
error: null,
1422
};
1523

16-
componentDidCatch(error) {
24+
componentDidCatch(error: Error): void {
1725
this.setState({ error });
1826
}
1927

20-
render() {
21-
return this.state.error ? <div className="error">{this.state.error}</div> : this.props.children;
28+
render(): React.ReactNode {
29+
return this.state.error ? <div className="error">{this.state.error.toString()}</div> : this.props.children;
2230
}
2331
}
2432

33+
interface AppComponentProps {
34+
app: {
35+
SDK: {
36+
mode: string;
37+
};
38+
crashed: boolean;
39+
loading: boolean;
40+
isLabeling: boolean;
41+
};
42+
}
43+
2544
/**
2645
* Main Component
27-
* @param {{app: import("../../stores/AppStore").AppStore} param0
2846
*/
29-
const AppComponent = ({ app }) => {
47+
const AppComponent: React.FC<AppComponentProps> = ({ app }) => {
3048
const rootCN = cn("root");
3149
const rootClassName = rootCN.mod({ mode: app.SDK.mode }).toString();
3250
const crashCN = cn("crash");
51+
3352
return (
3453
<ErrorBoundary>
3554
<Provider store={app}>

web/libs/editor/src/mixins/KonvaRegion.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { types } from "mobx-state-tree";
22
import { FF_DEV_3793, FF_ZOOM_OPTIM, isFF } from "../utils/feature-flags";
3+
import Constants from "../core/Constants";
4+
35
export const KonvaRegionMixin = types
46
.model({})
57
.views((self) => {
@@ -51,6 +53,28 @@ export const KonvaRegionMixin = types
5153
let deferredSelectId = null;
5254

5355
return {
56+
updateCursor(isHovered = false) {
57+
const stage = self.parent?.stageRef;
58+
if (!stage) return;
59+
const style = stage.container().style;
60+
61+
if (isHovered) {
62+
if (self.annotation.isLinkingMode) {
63+
style.cursor = Constants.LINKING_MODE_CURSOR;
64+
} else if (self.type !== "brushregion") {
65+
style.cursor = Constants.POINTER_CURSOR;
66+
}
67+
return;
68+
}
69+
70+
const selectedTool = self.parent?.getToolsManager().findSelectedTool();
71+
if (!selectedTool || !selectedTool.updateCursor) {
72+
style.cursor = Constants.DEFAULT_CURSOR;
73+
} else {
74+
selectedTool.updateCursor();
75+
}
76+
},
77+
5478
checkSizes() {
5579
const { naturalWidth, naturalHeight, stageWidth: width, stageHeight: height } = self.parent;
5680

web/libs/editor/src/regions/BrushRegion.jsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -704,20 +704,14 @@ const HtxBrushView = ({ item, setShapeRef }) => {
704704
onMouseOver={() => {
705705
if (store.annotationStore.selected.isLinkingMode) {
706706
item.setHighlight(true);
707-
stage.container().style.cursor = "crosshair";
708-
} else {
709-
// no tool selected
710-
if (!item.parent.getToolsManager().findSelectedTool()) stage.container().style.cursor = "pointer";
711707
}
708+
item.updateCursor(true);
712709
}}
713710
onMouseOut={() => {
714711
if (store.annotationStore.selected.isLinkingMode) {
715712
item.setHighlight(false);
716713
}
717-
718-
if (!item.parent?.getToolsManager().findSelectedTool()) {
719-
stage.container().style.cursor = "default";
720-
}
714+
item.updateCursor();
721715
}}
722716
onClick={(e) => {
723717
if (item.parent.getSkipInteractions()) return;

web/libs/editor/src/regions/EllipseRegion.jsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,17 +377,14 @@ const HtxEllipseView = ({ item, setShapeRef }) => {
377377
onMouseOver={() => {
378378
if (store.annotationStore.selected.isLinkingMode) {
379379
item.setHighlight(true);
380-
stage.container().style.cursor = Constants.LINKING_MODE_CURSOR;
381-
} else {
382-
stage.container().style.cursor = Constants.POINTER_CURSOR;
383380
}
381+
item.updateCursor(true);
384382
}}
385383
onMouseOut={() => {
386-
stage.container().style.cursor = Constants.DEFAULT_CURSOR;
387-
388384
if (store.annotationStore.selected.isLinkingMode) {
389385
item.setHighlight(false);
390386
}
387+
item.updateCursor();
391388
}}
392389
onClick={(e) => {
393390
if (item.parent.getSkipInteractions()) return;

web/libs/editor/src/regions/KeyPointRegion.jsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,14 @@ const HtxKeyPointView = ({ item, setShapeRef }) => {
258258
onMouseOver={() => {
259259
if (store.annotationStore.selected.isLinkingMode) {
260260
item.setHighlight(true);
261-
stage.container().style.cursor = "crosshair";
262-
} else {
263-
stage.container().style.cursor = "pointer";
264261
}
262+
item.updateCursor(true);
265263
}}
266264
onMouseOut={() => {
267-
stage.container().style.cursor = "default";
268-
269265
if (store.annotationStore.selected.isLinkingMode) {
270266
item.setHighlight(false);
271267
}
268+
item.updateCursor();
272269
}}
273270
onClick={(e) => {
274271
if (item.parent.getSkipInteractions()) return;

0 commit comments

Comments
 (0)