Skip to content

Commit e6497dc

Browse files
feat: FIT-718: Add disabled Databricks option with Enterprise badge on LSO (#8454) (#8466)
Co-authored-by: ricardoantoniocm <[email protected]>
1 parent 19e2cb6 commit e6497dc

File tree

13 files changed

+243
-33
lines changed

13 files changed

+243
-33
lines changed

web/apps/labelstudio/src/pages/Settings/StorageSettings/StorageSet.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const StorageSet = forwardRef(({ title, target, rootClass, buttonLabel },
3535
const modalRef = modal({
3636
title,
3737
closeOnClickOutside: false,
38-
style: { width: 960 },
38+
style: { width: 840 },
3939
bare: useNewStorageScreen,
4040
onHidden: () => {
4141
// Reset state when modal is closed (including Escape key)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { EnterpriseBadge, IconSpark } from "@humansignal/ui";
2+
import { Alert, AlertTitle, AlertDescription } from "@humansignal/shad/components/ui/alert";
3+
import { IconCloudProviderAzure } from "@humansignal/icons";
4+
import type { ProviderConfig } from "@humansignal/app-common/blocks/StorageProviderForm/types/provider";
5+
6+
const azureSpiProvider: ProviderConfig = {
7+
name: "azure_spi",
8+
title: "Azure Blob Storage\nwith Service Principal",
9+
description:
10+
"Configure your Azure Blob Storage connection using Service Principal authentication for enhanced security (proxy only)",
11+
icon: IconCloudProviderAzure,
12+
disabled: true,
13+
badge: <EnterpriseBadge />,
14+
fields: [
15+
{
16+
name: "enterprise_info",
17+
type: "message",
18+
content: (
19+
<Alert variant="gradient">
20+
<IconSpark />
21+
<AlertTitle>Enterprise Feature</AlertTitle>
22+
<AlertDescription>
23+
Azure Blob Storage with Service Principal is available in Label Studio Enterprise.{" "}
24+
<a
25+
href="https://docs.humansignal.com/guide/storage.html#Azure-Blob-Storage-with-Service-Principal-authentication"
26+
target="_blank"
27+
rel="noopener noreferrer"
28+
className="underline hover:no-underline"
29+
>
30+
Learn more
31+
</a>
32+
</AlertDescription>
33+
</Alert>
34+
),
35+
},
36+
],
37+
layout: [{ fields: ["enterprise_info"] }],
38+
};
39+
40+
export default azureSpiProvider;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { EnterpriseBadge, IconSpark } from "@humansignal/ui";
2+
import { Alert, AlertTitle, AlertDescription } from "@humansignal/shad/components/ui/alert";
3+
import { IconCloudProviderDatabricks } from "@humansignal/icons";
4+
import type { ProviderConfig } from "@humansignal/app-common/blocks/StorageProviderForm/types/provider";
5+
6+
const databricksProvider: ProviderConfig = {
7+
name: "databricks",
8+
title: "Databricks Files\n(UC Volumes)",
9+
description: "Configure your Databricks Unity Catalog Volumes connection with all required settings (proxy only)",
10+
icon: IconCloudProviderDatabricks,
11+
disabled: true,
12+
badge: <EnterpriseBadge />,
13+
fields: [
14+
{
15+
name: "enterprise_info",
16+
type: "message",
17+
content: (
18+
<Alert variant="gradient">
19+
<IconSpark />
20+
<AlertTitle>Enterprise Feature</AlertTitle>
21+
<AlertDescription>
22+
Databricks Files (UC Volumes) is available in Label Studio Enterprise.{" "}
23+
<a
24+
href="https://docs.humansignal.com/guide/storage.html#Databricks-Files-UC-Volumes"
25+
target="_blank"
26+
rel="noopener noreferrer"
27+
className="underline hover:no-underline"
28+
>
29+
Learn more
30+
</a>
31+
</AlertDescription>
32+
</Alert>
33+
),
34+
},
35+
],
36+
layout: [{ fields: ["enterprise_info"] }],
37+
};
38+
39+
export default databricksProvider;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { EnterpriseBadge, IconSpark } from "@humansignal/ui";
2+
import { Alert, AlertTitle, AlertDescription } from "@humansignal/shad/components/ui/alert";
3+
import { IconCloudProviderGCS } from "@humansignal/icons";
4+
import type { ProviderConfig } from "@humansignal/app-common/blocks/StorageProviderForm/types/provider";
5+
6+
const gcsWifProvider: ProviderConfig = {
7+
name: "gcswif",
8+
title: "Google Cloud Storage\n(WIF Auth)",
9+
description:
10+
"Configure your Google Cloud Storage connection with Workload Identity Federation authentication (proxy only)",
11+
icon: IconCloudProviderGCS,
12+
disabled: true,
13+
badge: <EnterpriseBadge />,
14+
fields: [
15+
{
16+
name: "enterprise_info",
17+
type: "message",
18+
content: (
19+
<Alert variant="gradient">
20+
<IconSpark />
21+
<AlertTitle>Enterprise Feature</AlertTitle>
22+
<AlertDescription>
23+
Google Cloud Storage with Workload Identity Federation is available in Label Studio Enterprise.{" "}
24+
<a
25+
href="https://docs.humansignal.com/guide/storage.html#Google-Cloud-Storage-with-Workload-Identity-Federation-WIF"
26+
target="_blank"
27+
rel="noopener noreferrer"
28+
className="underline hover:no-underline"
29+
>
30+
Learn more
31+
</a>
32+
</AlertDescription>
33+
</Alert>
34+
),
35+
},
36+
],
37+
layout: [{ fields: ["enterprise_info"] }],
38+
};
39+
40+
export default gcsWifProvider;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import azureProvider from "./azure";
2+
import azureSpiProvider from "./azure_spi";
3+
import databricksProvider from "./databricks";
24
import gcsProvider from "./gcs";
5+
import gcsWifProvider from "./gcswif";
36
import localFilesProvider from "./localFiles";
47
import redisProvider from "./redis";
58
import { s3Provider } from "./s3";
9+
import s3sProvider from "./s3s";
610

711
export const providers = {
12+
// Standard providers
813
s3: s3Provider,
914
gcs: gcsProvider,
1015
azure: azureProvider,
1116
redis: redisProvider,
17+
// Enterprise providers
18+
databricks: databricksProvider,
19+
s3s: s3sProvider,
20+
gcswif: gcsWifProvider,
21+
azure_spi: azureSpiProvider,
22+
// Local provider
1223
localfiles: localFilesProvider,
1324
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { EnterpriseBadge, IconSpark } from "@humansignal/ui";
2+
import { Alert, AlertTitle, AlertDescription } from "@humansignal/shad/components/ui/alert";
3+
import { IconCloudProviderS3 } from "@humansignal/icons";
4+
import type { ProviderConfig } from "@humansignal/app-common/blocks/StorageProviderForm/types/provider";
5+
6+
const s3sProvider: ProviderConfig = {
7+
name: "s3s",
8+
title: "Amazon S3\nwith IAM Role",
9+
description: "Configure your AWS S3 connection using IAM role access for enhanced security (proxy only)",
10+
icon: IconCloudProviderS3,
11+
disabled: true,
12+
badge: <EnterpriseBadge />,
13+
fields: [
14+
{
15+
name: "enterprise_info",
16+
type: "message",
17+
content: (
18+
<Alert variant="gradient">
19+
<IconSpark />
20+
<AlertTitle>Enterprise Feature</AlertTitle>
21+
<AlertDescription>
22+
Amazon S3 with IAM Role is available in Label Studio Enterprise.{" "}
23+
<a
24+
href="https://docs.humansignal.com/guide/storage.html#Set-up-an-S3-connection-with-IAM-role-access"
25+
target="_blank"
26+
rel="noopener noreferrer"
27+
className="underline hover:no-underline"
28+
>
29+
Learn more
30+
</a>
31+
</AlertDescription>
32+
</Alert>
33+
),
34+
},
35+
],
36+
layout: [{ fields: ["enterprise_info"] }],
37+
};
38+
39+
export default s3sProvider;

web/libs/app-common/src/blocks/StorageProviderForm/Steps/provider-selection-step.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,50 @@ export const ProviderSelectionStep = ({
2626
// Set default provider if none is selected and we have options
2727
useEffect(() => {
2828
if (!formData.provider && Object.entries(providers).length > 0) {
29-
handleSelectChange("provider", providers[0].name);
29+
// Find the first non-disabled provider
30+
const enabledProviders = Object.values(providers).filter((provider) => !provider.disabled);
31+
if (enabledProviders.length > 0) {
32+
handleSelectChange("provider", enabledProviders[0].name);
33+
}
3034
}
3135
}, [providers, formData.provider, handleSelectChange]);
3236

37+
// Get the selected provider config
38+
const selectedProvider = formData.provider ? providers[formData.provider] : null;
39+
const isSelectedProviderDisabled = selectedProvider?.disabled || false;
40+
41+
// Get the message content from the provider config
42+
const getMessageContent = () => {
43+
if (!selectedProvider?.fields) return null;
44+
45+
const messageField = selectedProvider.fields.find((field) => field.type === "message");
46+
return messageField?.content || null;
47+
};
48+
49+
const messageContent = getMessageContent();
50+
3351
return (
3452
<div className="space-y-6">
3553
<div>
3654
<h2 className="text-xl font-semibold">Choose your cloud storage provider</h2>
3755
<p className="text-muted-foreground">Select the cloud storage service where your data is stored</p>
3856
</div>
3957

40-
<div className="space-y-2">
41-
<Label htmlFor="provider" required>
42-
Storage Provider
43-
</Label>
44-
<ProviderGrid
45-
providers={providers}
46-
selectedProvider={formData.provider}
47-
onProviderSelect={(providerName) => handleSelectChange("provider", providerName)}
48-
error={errors.provider}
49-
/>
58+
<div className="space-y-4">
59+
<div className="space-y-2">
60+
<Label text="Storage Provider" required />
61+
<ProviderGrid
62+
providers={providers}
63+
selectedProvider={formData.provider}
64+
onProviderSelect={(providerName) => handleSelectChange("provider", providerName)}
65+
error={errors.provider}
66+
/>
67+
</div>
68+
69+
{/* Show alert message when disabled provider is selected */}
70+
{isSelectedProviderDisabled && messageContent && (
71+
<div>{typeof messageContent === "function" ? messageContent({}) : messageContent}</div>
72+
)}
5073
</div>
5174
</div>
5275
);

web/libs/app-common/src/blocks/StorageProviderForm/components/form-footer.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface FormFooterProps {
2424
isLoading: boolean;
2525
};
2626
target?: "import" | "export";
27+
isProviderDisabled?: boolean;
2728
}
2829

2930
export const FormFooter = ({
@@ -40,6 +41,7 @@ export const FormFooter = ({
4041
createStorage,
4142
saveStorage,
4243
target,
44+
isProviderDisabled = false,
4345
}: FormFooterProps) => {
4446
return (
4547
<div className="flex items-center justify-between p-wide border-t border-neutral-border bg-neutral-background">
@@ -74,9 +76,17 @@ export const FormFooter = ({
7476
<Button
7577
onClick={onNext}
7678
waiting={currentStep === totalSteps - 1 && createStorage.isLoading}
77-
disabled={!isEditMode && currentStep === 1 && !connectionChecked}
79+
disabled={
80+
(!isEditMode && currentStep === 1 && !connectionChecked) || (currentStep === 0 && isProviderDisabled)
81+
}
7882
look={currentStep === totalSteps - 1 && target !== "export" ? "outlined" : undefined}
79-
tooltip={currentStep === 1 && !connectionChecked ? "Test connection before continuing" : undefined}
83+
tooltip={
84+
currentStep === 1 && !connectionChecked
85+
? "Test connection before continuing"
86+
: currentStep === 0 && isProviderDisabled
87+
? "This provider is not available in the current version"
88+
: undefined
89+
}
8090
>
8191
{currentStep < totalSteps - 1 ? "Next" : target === "export" ? "Save" : "Save & Sync"}
8292
</Button>

web/libs/app-common/src/blocks/StorageProviderForm/components/provider-grid.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,48 @@
11
import { cn } from "@humansignal/ui";
22
import type { ProviderConfig } from "../types/provider";
33

4-
interface Provider {
5-
name: string;
6-
title: string;
7-
}
8-
94
interface ProviderGridProps {
10-
providers: ProviderConfig[];
5+
providers: Record<string, ProviderConfig>;
116
selectedProvider?: string;
127
onProviderSelect: (providerName: string) => void;
138
error?: string;
149
}
1510

1611
export const ProviderGrid = ({ providers, selectedProvider, onProviderSelect, error }: ProviderGridProps) => {
1712
return (
18-
<div className="flex flex-col gap-2">
19-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-tight">
13+
<div className="flex flex-col gap-tight">
14+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-base">
2015
{Object.entries(providers).map(([_, provider]) => {
2116
const isSelected = selectedProvider === provider.name;
17+
const isDisabled = provider.disabled;
2218
const Icon = provider.icon;
2319

2420
return (
2521
<button
2622
key={provider.name}
2723
type="button"
2824
onClick={() => onProviderSelect(provider.name)}
25+
data-testid={`storage-provider-${provider.name}`}
2926
className={cn(
30-
"relative p-base border-2 rounded-lg transition-all duration-200 text-center",
31-
"hover:border-primary-border hover:bg-primary-emphasis-subtle",
32-
"focus:outline-none focus:ring-2 focus:ring-primary-focus-outline focus:ring-offset-2",
33-
"flex flex-col items-center gap-2",
27+
"relative p-base border-2 rounded-lg transition-all duration-200 text-center min-h-[120px]",
28+
"flex flex-col items-center gap-tight relative text-neutral-content",
29+
"hover:border-primary-border-subtle hover:bg-primary-emphasis-subtle",
30+
"hover:-translate-y-tightest focus:outline-none focus:ring-2 focus:ring-primary-focus-outline focus:ring-offset-2",
3431
isSelected
35-
? "border-primary-border bg-primary-emphasis-subtle shadow-sm"
32+
? "border-primary-border-subtle bg-primary-emphasis-subtle shadow-sm"
3633
: "border-neutral-border hover:border-primary-border-subtle",
3734
)}
3835
aria-pressed={isSelected}
36+
aria-disabled={isDisabled}
3937
>
4038
{Icon && <Icon className="w-8 h-8" />}
41-
<div className="flex-1 min-w-0">
42-
<h3 className="text-body-medium text-neutral-content truncate whitespace-pre">{provider.title}</h3>
39+
<div className="flex-1 min-w-0 text-center">
40+
<h3 className="text-body-medium truncate whitespace-pre">{provider.title}</h3>
41+
{provider.badge && (
42+
<div className="mt-1 flex justify-center whitespace-pre absolute -bottom-tight left-1/2 -translate-x-[40px]">
43+
{provider.badge}
44+
</div>
45+
)}
4346
</div>
4447
</button>
4548
);

web/libs/app-common/src/blocks/StorageProviderForm/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ export const StorageProviderForm = forwardRef<unknown, StorageProviderFormProps>
342342
isLoading: saveStorageMutation.isLoading,
343343
}}
344344
target={effectiveTarget}
345+
isProviderDisabled={providers[formData.provider]?.disabled || false}
345346
/>
346347
</div>
347348
);

0 commit comments

Comments
 (0)