Skip to content

Commit 1ed9349

Browse files
Copilotlstein
andauthored
feat: distinct splash screens for admin/non-admin users in multiuser mode (#116)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
1 parent 6b57b00 commit 1ed9349

11 files changed

Lines changed: 177 additions & 23 deletions

File tree

invokeai/app/api/routers/auth.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class SetupStatusResponse(BaseModel):
7272

7373
setup_required: bool = Field(description="Whether initial setup is required")
7474
multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
75+
admin_email: str | None = Field(default=None, description="Email of the first active admin user, if any")
7576

7677

7778
@auth_router.get("/status", response_model=SetupStatusResponse)
@@ -85,13 +86,14 @@ async def get_setup_status() -> SetupStatusResponse:
8586

8687
# If multiuser is disabled, setup is never required
8788
if not config.multiuser:
88-
return SetupStatusResponse(setup_required=False, multiuser_enabled=False)
89+
return SetupStatusResponse(setup_required=False, multiuser_enabled=False, admin_email=None)
8990

9091
# In multiuser mode, check if an admin exists
9192
user_service = ApiDependencies.invoker.services.users
9293
setup_required = not user_service.has_admin()
94+
admin_email = user_service.get_admin_email()
9395

94-
return SetupStatusResponse(setup_required=setup_required, multiuser_enabled=True)
96+
return SetupStatusResponse(setup_required=setup_required, multiuser_enabled=True, admin_email=admin_email)
9597

9698

9799
@auth_router.post("/login", response_model=LoginResponse)

invokeai/app/services/users/users_base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,12 @@ def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
124124
List of users
125125
"""
126126
pass
127+
128+
@abstractmethod
129+
def get_admin_email(self) -> str | None:
130+
"""Get the email address of the first active admin user.
131+
132+
Returns:
133+
Email address of the first active admin, or None if no admin exists
134+
"""
135+
pass

invokeai/app/services/users/users_default.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,17 @@ def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
249249
)
250250
for row in rows
251251
]
252+
253+
def get_admin_email(self) -> str | None:
254+
"""Get the email address of the first active admin user."""
255+
with self._db.transaction() as cursor:
256+
cursor.execute(
257+
"""
258+
SELECT email FROM users
259+
WHERE is_admin = TRUE AND is_active = TRUE
260+
ORDER BY created_at ASC
261+
LIMIT 1
262+
""",
263+
)
264+
row = cursor.fetchone()
265+
return row[0] if row else None

invokeai/frontend/web/public/locales/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,9 @@
10621062
"name": "Name",
10631063
"modelPickerFallbackNoModelsInstalled": "No models installed.",
10641064
"modelPickerFallbackNoModelsInstalled2": "Visit the <LinkComponent>Model Manager</LinkComponent> to install models.",
1065+
"modelPickerFallbackNoModelsInstalledNonAdmin": "No models installed. Ask your InvokeAI administrator (<AdminEmailLink />) to install some models.",
10651066
"noModelsInstalledDesc1": "Install models with the",
1067+
"noModelsInstalledAskAdmin": "Ask your administrator to install some.",
10661068
"noModelSelected": "No Model Selected",
10671069
"noMatchingModels": "No matching models",
10681070
"noModelsInstalled": "No models installed",
@@ -2863,6 +2865,7 @@
28632865
"tileOverlap": "Tile Overlap",
28642866
"postProcessingMissingModelWarning": "Visit the <LinkComponent>Model Manager</LinkComponent> to install a post-processing (image to image) model.",
28652867
"missingModelsWarning": "Visit the <LinkComponent>Model Manager</LinkComponent> to install the required models:",
2868+
"missingModelsWarningNonAdmin": "Ask your InvokeAI administrator (<AdminEmailLink />) to install the required models:",
28662869
"mainModelDesc": "Main model (SD1.5 or SDXL architecture)",
28672870
"tileControlNetModelDesc": "Tile ControlNet model for the chosen main model architecture",
28682871
"upscaleModelDesc": "Upscale (image to image) model",
@@ -2971,6 +2974,7 @@
29712974
},
29722975
"workflows": {
29732976
"description": "Workflows are reusable templates that automate image generation tasks, allowing you to quickly perform complex operations and get consistent results.",
2977+
"descriptionMultiuser": "Workflows are reusable templates that automate image generation tasks, allowing you to quickly perform complex operations and get consistent results. You may share your workflows with other users of the system by selecting 'Shared workflow' when you create or edit it.",
29742978
"learnMoreLink": "Learn more about creating workflows",
29752979
"browseTemplates": {
29762980
"title": "Browse Workflow Templates",
@@ -3049,9 +3053,11 @@
30493053
"toGetStartedLocal": "To get started, make sure to download or import models needed to run Invoke. Then, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
30503054
"toGetStarted": "To get started, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
30513055
"toGetStartedWorkflow": "To get started, fill in the fields on the left and press <StrongComponent>Invoke</StrongComponent> to generate your image. Want to explore more workflows? Click the <StrongComponent>folder icon</StrongComponent> next to the workflow title to see a list of other templates you can try.",
3056+
"toGetStartedNonAdmin": "To get started, ask your InvokeAI administrator (<AdminEmailLink />) to install the AI models needed to run Invoke. Then, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. Select a prompt template to improve results. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
30523057
"gettingStartedSeries": "Want more guidance? Check out our <LinkComponent>Getting Started Series</LinkComponent> for tips on unlocking the full potential of the Invoke Studio.",
30533058
"lowVRAMMode": "For best performance, follow our <LinkComponent>Low VRAM guide</LinkComponent>.",
3054-
"noModelsInstalled": "It looks like you don't have any models installed! You can <DownloadStarterModelsButton>download a starter model bundle</DownloadStarterModelsButton> or <ImportModelsButton>import models</ImportModelsButton>."
3059+
"noModelsInstalled": "It looks like you don't have any models installed! You can <DownloadStarterModelsButton>download a starter model bundle</DownloadStarterModelsButton> or <ImportModelsButton>import models</ImportModelsButton>.",
3060+
"noModelsInstalledAskAdmin": "Ask your administrator to install some."
30553061
},
30563062
"whatsNew": {
30573063
"whatsNewInInvoke": "What's New in Invoke",

invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
import type { ButtonProps } from '@invoke-ai/ui-library';
22
import { Alert, AlertDescription, AlertIcon, Button, Divider, Flex, Link, Spinner, Text } from '@invoke-ai/ui-library';
3+
import { useAppSelector } from 'app/store/storeHooks';
34
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
45
import { InvokeLogoIcon } from 'common/components/InvokeLogoIcon';
6+
import { selectCurrentUser } from 'features/auth/store/authSlice';
57
import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImages';
68
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
79
import { navigationApi } from 'features/ui/layouts/navigation-api';
810
import type { PropsWithChildren } from 'react';
911
import { memo, useCallback, useMemo } from 'react';
1012
import { Trans, useTranslation } from 'react-i18next';
1113
import { PiArrowSquareOutBold, PiImageBold } from 'react-icons/pi';
14+
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
1215
import { useMainModels } from 'services/api/hooks/modelsByType';
1316

1417
export const NoContentForViewer = memo(() => {
1518
const hasImages = useHasImages();
1619
const [mainModels, { data }] = useMainModels();
20+
const { data: setupStatus } = useGetSetupStatusQuery();
21+
const user = useAppSelector(selectCurrentUser);
1722
const { t } = useTranslation();
1823

24+
const isMultiuser = setupStatus?.multiuser_enabled ?? false;
25+
const isAdmin = !isMultiuser || (user?.is_admin ?? false);
26+
const adminEmail = setupStatus?.admin_email ?? null;
27+
28+
const modelsLoaded = data !== undefined;
29+
const hasModels = mainModels.length > 0;
30+
1931
const showStarterBundles = useMemo(() => {
20-
return data && mainModels.length === 0;
21-
}, [mainModels.length, data]);
32+
return modelsLoaded && !hasModels && isAdmin;
33+
}, [modelsLoaded, hasModels, isAdmin]);
2234

2335
if (hasImages === LOADING_SYMBOL) {
2436
// Blank bg w/ a spinner. The new user experience components below have an invoke logo, but it's not centered.
@@ -36,10 +48,18 @@ export const NoContentForViewer = memo(() => {
3648
<Flex flexDir="column" gap={8} alignItems="center" textAlign="center" maxW="400px">
3749
<InvokeLogoIcon w={32} h={32} />
3850
<Flex flexDir="column" gap={4} alignItems="center" textAlign="center">
39-
<GetStartedLocal />
40-
{showStarterBundles && <StarterBundlesCallout />}
41-
<Divider />
42-
<LowVRAMAlert />
51+
{isAdmin ? (
52+
// Admin / single-user mode
53+
<>
54+
{modelsLoaded && hasModels ? <GetStartedWithModels /> : <GetStartedLocal />}
55+
{showStarterBundles && <StarterBundlesCallout />}
56+
<Divider />
57+
<LowVRAMAlert />
58+
</>
59+
) : (
60+
// Non-admin user in multiuser mode
61+
<>{modelsLoaded && hasModels ? <GetStartedWithModels /> : <GetStartedNonAdmin adminEmail={adminEmail} />}</>
62+
)}
4363
</Flex>
4464
</Flex>
4565
);
@@ -89,6 +109,32 @@ const GetStartedLocal = () => {
89109
);
90110
};
91111

112+
const GetStartedWithModels = () => {
113+
return (
114+
<Text fontSize="md" color="base.200">
115+
<Trans i18nKey="newUserExperience.toGetStarted" components={{ StrongComponent }} />
116+
</Text>
117+
);
118+
};
119+
120+
const GetStartedNonAdmin = ({ adminEmail }: { adminEmail: string | null }) => {
121+
const AdminEmailLink = adminEmail ? (
122+
<Link href={`mailto:${adminEmail}`} color="base.50">
123+
{adminEmail}
124+
</Link>
125+
) : (
126+
<Text as="span" color="base.50">
127+
your administrator
128+
</Text>
129+
);
130+
131+
return (
132+
<Text fontSize="md" color="base.200">
133+
<Trans i18nKey="newUserExperience.toGetStartedNonAdmin" components={{ StrongComponent, AdminEmailLink }} />
134+
</Text>
135+
);
136+
};
137+
92138
const StarterBundlesCallout = () => {
93139
const handleClickDownloadStarterModels = useCallback(() => {
94140
navigationApi.switchToTab('models');

invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Button, Text, useToast } from '@invoke-ai/ui-library';
22
import { useAppSelector } from 'app/store/storeHooks';
3-
import { selectIsAuthenticated } from 'features/auth/store/authSlice';
3+
import { selectCurrentUser, selectIsAuthenticated } from 'features/auth/store/authSlice';
44
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
55
import { navigationApi } from 'features/ui/layouts/navigation-api';
66
import { useCallback, useEffect, useState } from 'react';
77
import { useTranslation } from 'react-i18next';
8+
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
89
import { useMainModels } from 'services/api/hooks/modelsByType';
910

1011
const TOAST_ID = 'starterModels';
@@ -15,6 +16,11 @@ export const useStarterModelsToast = () => {
1516
const [mainModels, { data }] = useMainModels();
1617
const toast = useToast();
1718
const isAuthenticated = useAppSelector(selectIsAuthenticated);
19+
const { data: setupStatus } = useGetSetupStatusQuery();
20+
const user = useAppSelector(selectCurrentUser);
21+
22+
const isMultiuser = setupStatus?.multiuser_enabled ?? false;
23+
const isAdmin = !isMultiuser || (user?.is_admin ?? false);
1824

1925
useEffect(() => {
2026
// Only show the toast if the user is authenticated
@@ -33,17 +39,17 @@ export const useStarterModelsToast = () => {
3339
toast({
3440
id: TOAST_ID,
3541
title: t('modelManager.noModelsInstalled'),
36-
description: <ToastDescription />,
42+
description: isAdmin ? <AdminToastDescription /> : <NonAdminToastDescription />,
3743
status: 'info',
3844
isClosable: true,
3945
duration: null,
4046
onCloseComplete: () => setDidToast(true),
4147
});
4248
}
43-
}, [data, didToast, isAuthenticated, mainModels.length, t, toast]);
49+
}, [data, didToast, isAuthenticated, isAdmin, mainModels.length, t, toast]);
4450
};
4551

46-
const ToastDescription = () => {
52+
const AdminToastDescription = () => {
4753
const { t } = useTranslation();
4854
const toast = useToast();
4955

@@ -62,3 +68,9 @@ const ToastDescription = () => {
6268
</Text>
6369
);
6470
};
71+
72+
const NonAdminToastDescription = () => {
73+
const { t } = useTranslation();
74+
75+
return <Text fontSize="md">{t('modelManager.noModelsInstalledAskAdmin')}</Text>;
76+
};

invokeai/frontend/web/src/features/parameters/components/ModelPicker.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Button,
44
Flex,
55
Icon,
6+
Link,
67
Popover,
78
PopoverArrow,
89
PopoverBody,
@@ -20,6 +21,7 @@ import { buildGroup, getRegex, isGroup, Picker, usePickerContext } from 'common/
2021
import { useDisclosure } from 'common/hooks/useBoolean';
2122
import { typedMemo } from 'common/util/typedMemo';
2223
import { uniq } from 'es-toolkit/compat';
24+
import { selectCurrentUser } from 'features/auth/store/authSlice';
2325
import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
2426
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
2527
import { MODEL_BASE_TO_COLOR, MODEL_BASE_TO_LONG_NAME, MODEL_BASE_TO_SHORT_NAME } from 'features/modelManagerV2/models';
@@ -32,6 +34,7 @@ import { filesize } from 'filesize';
3234
import { memo, useCallback, useMemo, useRef } from 'react';
3335
import { Trans, useTranslation } from 'react-i18next';
3436
import { PiCaretDownBold, PiLinkSimple } from 'react-icons/pi';
37+
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
3538
import { useGetRelatedModelIdsBatchQuery } from 'services/api/endpoints/modelRelationships';
3639
import type { AnyModelConfig } from 'services/api/types';
3740

@@ -82,6 +85,32 @@ const components = {
8285

8386
const NoOptionsFallback = memo(({ noOptionsText }: { noOptionsText?: string }) => {
8487
const { t } = useTranslation();
88+
const { data: setupStatus } = useGetSetupStatusQuery();
89+
const user = useAppSelector(selectCurrentUser);
90+
91+
const isMultiuser = setupStatus?.multiuser_enabled ?? false;
92+
const isAdmin = !isMultiuser || (user?.is_admin ?? false);
93+
const adminEmail = setupStatus?.admin_email ?? null;
94+
95+
if (!isAdmin) {
96+
const AdminEmailLink = adminEmail ? (
97+
<Link href={`mailto:${adminEmail}`} color="base.200">
98+
{adminEmail}
99+
</Link>
100+
) : (
101+
<Text as="span" color="base.200">
102+
your administrator
103+
</Text>
104+
);
105+
106+
return (
107+
<Flex flexDir="column" gap={4} alignItems="center">
108+
<Text color="base.200" textAlign="center">
109+
<Trans i18nKey="modelManager.modelPickerFallbackNoModelsInstalledNonAdmin" components={{ AdminEmailLink }} />
110+
</Text>
111+
</Flex>
112+
);
113+
}
85114

86115
return (
87116
<Flex flexDir="column" gap={4} alignItems="center">

invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Button, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
1+
import { Button, Flex, Link, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library';
22
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
3+
import { selectCurrentUser } from 'features/auth/store/authSlice';
34
import { selectModel } from 'features/controlLayers/store/paramsSlice';
45
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
56
import {
@@ -10,6 +11,7 @@ import {
1011
import { navigationApi } from 'features/ui/layouts/navigation-api';
1112
import { useCallback, useEffect, useMemo } from 'react';
1213
import { Trans, useTranslation } from 'react-i18next';
14+
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
1315
import { useControlNetModels } from 'services/api/hooks/modelsByType';
1416

1517
export const UpscaleWarning = () => {
@@ -19,6 +21,12 @@ export const UpscaleWarning = () => {
1921
const tileControlnetModel = useAppSelector(selectTileControlNetModel);
2022
const dispatch = useAppDispatch();
2123
const [modelConfigs, { isLoading }] = useControlNetModels();
24+
const { data: setupStatus } = useGetSetupStatusQuery();
25+
const user = useAppSelector(selectCurrentUser);
26+
27+
const isMultiuser = setupStatus?.multiuser_enabled ?? false;
28+
const isAdmin = !isMultiuser || (user?.is_admin ?? false);
29+
const adminEmail = setupStatus?.admin_email ?? null;
2230

2331
useEffect(() => {
2432
const validModel = modelConfigs.find((cnetModel) => {
@@ -59,19 +67,33 @@ export const UpscaleWarning = () => {
5967
return null;
6068
}
6169

70+
const AdminEmailLink = adminEmail ? (
71+
<Link href={`mailto:${adminEmail}`} color="base.50">
72+
{adminEmail}
73+
</Link>
74+
) : (
75+
<Text as="span" color="base.50">
76+
your administrator
77+
</Text>
78+
);
79+
6280
return (
6381
<Flex bg="error.500" borderRadius="base" padding={4} direction="column" fontSize="sm" gap={2}>
6482
{!isBaseModelCompatible && <Text>{t('upscaling.incompatibleBaseModelDesc')}</Text>}
6583
{warnings.length > 0 && (
6684
<Text>
67-
<Trans
68-
i18nKey="upscaling.missingModelsWarning"
69-
components={{
70-
LinkComponent: (
71-
<Button size="sm" flexGrow={0} variant="link" color="base.50" onClick={handleGoToModelManager} />
72-
),
73-
}}
74-
/>
85+
{isAdmin ? (
86+
<Trans
87+
i18nKey="upscaling.missingModelsWarning"
88+
components={{
89+
LinkComponent: (
90+
<Button size="sm" flexGrow={0} variant="link" color="base.50" onClick={handleGoToModelManager} />
91+
),
92+
}}
93+
/>
94+
) : (
95+
<Trans i18nKey="upscaling.missingModelsWarningNonAdmin" components={{ AdminEmailLink }} />
96+
)}
7597
</Text>
7698
)}
7799
{warnings.length > 0 && (

0 commit comments

Comments
 (0)