Compare commits

...

12 Commits

Author SHA1 Message Date
76f56c00df WIP pending https://github.com/bvaughn/react-resizable-panels/issues/226 2023-12-06 16:54:43 +11:00
e007dbf32f hide workflow library buttons if feature is disabled 2023-12-05 12:18:27 -05:00
7e36343cf1 Merge branch 'main' into feat/workflow-saving 2023-12-05 22:57:28 +11:00
748f94e0e3 fix(workflow_records): default category to WorkflowCategory.User
This allows old workflows to validate when reading them from the db or image files.
2023-12-05 22:55:53 +11:00
e990235d32 translationBot(ui): update translation (Korean)
Currently translated at 5.2% (70 of 1321 strings)

Co-authored-by: 이승석 <vidicwb@ajou.ac.kr>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ko/
Translation: InvokeAI/Web UI
2023-12-05 16:00:03 +11:00
5f122186bd translationBot(ui): update translation (Chinese (Simplified))
Currently translated at 99.8% (1317 of 1319 strings)

Co-authored-by: Surisen <zhonghx0804@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/zh_Hans/
Translation: InvokeAI/Web UI
2023-12-05 16:00:03 +11:00
1ca0901cbe Ensure that fetching a logger doesn't reset its loglevel to default (#5222)
## What type of PR is this? (check all applicable)

- [ ] Refactor
- [ ] Feature
- [x] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [ ] Yes
- [X] No, because: minor bug

      
## Have you updated all relevant documentation?
- [X] Yes
- [ ] No


## Description

While writing regression tests for the queued downloader I discovered
that when using `InvokeAILogger.get_logger()` to fetch a
previously-created logger it resets that logger's log level to the
default specified in the global config. In other words, this didn't work
as expected:

```
import logging
from invokeai.backend.util.logging import InvokeAILogger
logger1 = InvokeAILogger.get_logger('TestLogger')
logger1.setLevel(logging.DEBUG)
logger2 = InvokeAILogger.get_logger('TestLogger')
assert logger1.level == logging.DEBUG
assert logger2.level == logging.DEBUG
```

This PR fixes the problem and adds a corresponding pytest.

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below. 

For example having the text: "closes #1234" would connect the current
pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## QA Instructions, Screenshots, Recordings

<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Added/updated tests?

- [X] Yes
- [ ] No

## [optional] Are there any post deployment tasks we need to perform?
2023-12-04 22:50:59 -05:00
2d7555b7b8 Merge branch 'bugfix/log-levels' of github.com:invoke-ai/InvokeAI into bugfix/log-levels 2023-12-04 22:42:06 -05:00
3c7d1fcd32 clean up get_logger() call 2023-12-04 22:41:59 -05:00
c7fa2db556 Merge branch 'main' into bugfix/log-levels 2023-12-04 22:01:42 -05:00
bdb0d13a2d fix import order 2023-12-02 11:56:41 -05:00
2d2ef5d72c ensure that setting loglevel on one logger doesn't change others 2023-12-02 11:48:51 -05:00
14 changed files with 265 additions and 92 deletions

View File

@ -36,7 +36,9 @@ class WorkflowCategory(str, Enum, metaclass=MetaEnum):
class WorkflowMeta(BaseModel):
version: str = Field(description="The version of the workflow schema.")
category: WorkflowCategory = Field(description="The category of the workflow (user or default).")
category: WorkflowCategory = Field(
default=WorkflowCategory.User, description="The category of the workflow (user or default)."
)
@field_validator("version")
def validate_version(cls, version: str):

View File

@ -342,14 +342,13 @@ class InvokeAILogger(object): # noqa D102
cls, name: str = "InvokeAI", config: InvokeAIAppConfig = InvokeAIAppConfig.get_config()
) -> logging.Logger: # noqa D102
if name in cls.loggers:
logger = cls.loggers[name]
logger.handlers.clear()
else:
logger = logging.getLogger(name)
return cls.loggers[name]
logger = logging.getLogger(name)
logger.setLevel(config.log_level.upper()) # yes, strings work here
for ch in cls.get_loggers(config):
logger.addHandler(ch)
cls.loggers[name] = logger
cls.loggers[name] = logger
return cls.loggers[name]
@classmethod
@ -358,7 +357,7 @@ class InvokeAILogger(object): # noqa D102
handlers = []
for handler in handler_strs:
handler_name, *args = handler.split("=", 2)
args = args[0] if len(args) > 0 else None
arg = args[0] if len(args) > 0 else None
# console and file get the fancy formatter.
# syslog gets a simple one
@ -370,16 +369,16 @@ class InvokeAILogger(object): # noqa D102
handlers.append(ch)
elif handler_name == "syslog":
ch = cls._parse_syslog_args(args)
ch = cls._parse_syslog_args(arg)
handlers.append(ch)
elif handler_name == "file":
ch = cls._parse_file_args(args)
ch = cls._parse_file_args(arg)
ch.setFormatter(formatter())
handlers.append(ch)
elif handler_name == "http":
ch = cls._parse_http_args(args)
ch = cls._parse_http_args(arg)
handlers.append(ch)
return handlers

View File

@ -94,7 +94,7 @@
"react-icons": "^4.11.0",
"react-konva": "^18.2.10",
"react-redux": "^8.1.3",
"react-resizable-panels": "^0.0.55",
"react-resizable-panels": "^0.0.63",
"react-use": "^17.4.0",
"react-virtuoso": "^4.6.2",
"reactflow": "^11.9.4",

View File

@ -72,5 +72,13 @@
},
"unifiedCanvas": {
"betaPreserveMasked": "마스크 레이어 유지"
},
"accessibility": {
"previousImage": "이전 이미지",
"modifyConfig": "Config 수정",
"nextImage": "다음 이미지",
"mode": "모드",
"menu": "메뉴",
"modelSelect": "모델 선택"
}
}

View File

@ -99,7 +99,17 @@
"data": "数据",
"safetensors": "Safetensors",
"outpaint": "外扩绘制",
"details": "详情"
"details": "详情",
"format": "格式",
"unknown": "未知",
"folder": "文件夹",
"error": "错误",
"installed": "已安装",
"file": "文件",
"somethingWentWrong": "出了点问题",
"copyError": "$t(gallery.copy) 错误",
"input": "输入",
"notInstalled": "非 $t(common.installed)"
},
"gallery": {
"generations": "生成的图像",
@ -130,7 +140,12 @@
"preparingDownload": "准备下载",
"preparingDownloadFailed": "准备下载时出现问题",
"downloadSelection": "下载所选内容",
"noImageSelected": "无选中的图像"
"noImageSelected": "无选中的图像",
"deleteSelection": "删除所选内容",
"image": "图像",
"drop": "弃用",
"dropOrUpload": "$t(gallery.drop) 或上传",
"dropToUpload": "$t(gallery.drop) 以上传"
},
"hotkeys": {
"keyboardShortcuts": "键盘快捷键",
@ -486,7 +501,8 @@
"alpha": "Alpha",
"vaePrecision": "VAE 精度",
"checkpointOrSafetensors": "$t(common.checkpoint) / $t(common.safetensors)",
"noModelSelected": "无选中的模型"
"noModelSelected": "无选中的模型",
"conversionNotSupported": "转换尚未支持"
},
"parameters": {
"images": "图像",
@ -615,7 +631,10 @@
"seamlessX": "无缝 X",
"seamlessY": "无缝 Y",
"maskEdge": "遮罩边缘",
"unmasked": "取消遮罩"
"unmasked": "取消遮罩",
"cfgRescaleMultiplier": "CFG 重缩放倍数",
"cfgRescale": "CFG 重缩放",
"useSize": "使用尺寸"
},
"settings": {
"models": "模型",
@ -655,7 +674,8 @@
"clearIntermediatesDisabled": "队列为空才能清理中间产物",
"enableNSFWChecker": "启用成人内容检测器",
"enableInvisibleWatermark": "启用不可见水印",
"enableInformationalPopovers": "启用信息弹窗"
"enableInformationalPopovers": "启用信息弹窗",
"reloadingIn": "重新加载中"
},
"toast": {
"tempFoldersEmptied": "临时文件夹已清空",
@ -739,7 +759,8 @@
"imageUploadFailed": "图像上传失败",
"problemImportingMask": "导入遮罩时出现问题",
"baseModelChangedCleared_other": "基础模型已更改, 已清除或禁用 {{count}} 个不兼容的子模型",
"setAsCanvasInitialImage": "设为画布初始图像"
"setAsCanvasInitialImage": "设为画布初始图像",
"invalidUpload": "无效的上传"
},
"unifiedCanvas": {
"layer": "图层",
@ -748,7 +769,7 @@
"maskingOptions": "遮罩选项",
"enableMask": "启用遮罩",
"preserveMaskedArea": "保留遮罩区域",
"clearMask": "清除遮罩",
"clearMask": "清除遮罩 (Shift+C)",
"brush": "刷子",
"eraser": "橡皮擦",
"fillBoundingBox": "填充选择区域",
@ -801,7 +822,8 @@
"betaPreserveMasked": "保留遮罩层",
"antialiasing": "抗锯齿",
"showResultsOn": "显示结果 (开)",
"showResultsOff": "显示结果 (关)"
"showResultsOff": "显示结果 (关)",
"saveMask": "保存 $t(unifiedCanvas.mask)"
},
"accessibility": {
"modelSelect": "模型选择",
@ -826,7 +848,9 @@
"menu": "菜单",
"showGalleryPanel": "显示图库浮窗",
"loadMore": "加载更多",
"mode": "模式"
"mode": "模式",
"resetUI": "$t(accessibility.reset) UI",
"createIssue": "创建问题"
},
"ui": {
"showProgressImages": "显示处理中的图片",
@ -877,7 +901,7 @@
"animatedEdges": "边缘动效",
"nodeTemplate": "节点模板",
"pickOne": "选择一个",
"unableToLoadWorkflow": "无法验证工作流",
"unableToLoadWorkflow": "无法加载工作流",
"snapToGrid": "对齐网格",
"noFieldsLinearview": "线性视图中未添加任何字段",
"nodeSearch": "检索节点",
@ -929,7 +953,7 @@
"skippingUnknownOutputType": "跳过未知类型的输出",
"latentsFieldDescription": "Latents 可以在节点间传递。",
"denoiseMaskFieldDescription": "去噪遮罩可以在节点间传递",
"missingTemplate": "缺失模板",
"missingTemplate": "无效的节点:类型为 {{type}} 的节点 {{node}} 缺失模板(无已安装模板?)",
"outputSchemaNotFound": "未找到输出模式",
"latentsPolymorphicDescription": "Latents 可以在节点间传递。",
"colorFieldDescription": "一种 RGBA 颜色。",
@ -957,7 +981,7 @@
"collectionItem": "项目合集",
"controlCollectionDescription": "节点间传递的控制信息。",
"skippedReservedInput": "跳过保留的输入",
"outputFields": "输出",
"outputFields": "输出区域",
"edge": "边缘",
"inputNode": "输入节点",
"enumDescription": "枚举 (Enums) 可能是多个选项的一个数值。",
@ -992,7 +1016,7 @@
"string": "字符串",
"inputFields": "输入",
"uNetFieldDescription": "UNet 子模型。",
"mismatchedVersion": "不匹配的版本",
"mismatchedVersion": "无效的节点:类型为 {{type}} 的节点 {{node}} 版本不匹配(是否尝试更新?)",
"vaeFieldDescription": "Vae 子模型。",
"imageFieldDescription": "图像可以在节点间传递。",
"outputNode": "输出节点",
@ -1050,8 +1074,36 @@
"latentsPolymorphic": "Latents 多态",
"conditioningField": "条件",
"latentsField": "Latents",
"updateAllNodes": "更新所有节点",
"unableToUpdateNodes_other": "{{count}} 个节点无法完成更新"
"updateAllNodes": "更新节点",
"unableToUpdateNodes_other": "{{count}} 个节点无法完成更新",
"inputFieldTypeParseError": "无法解析 {{node}} 的输入类型 {{field}}。({{message}})",
"unsupportedArrayItemType": "不支持的数组类型 \"{{type}}\"",
"addLinearView": "添加到线性视图",
"targetNodeFieldDoesNotExist": "无效的边缘:{{node}} 的目标/输入区域 {{field}} 不存在",
"unsupportedMismatchedUnion": "合集或标量类型与基类 {{firstType}} 和 {{secondType}} 不匹配",
"allNodesUpdated": "已更新所有节点",
"sourceNodeDoesNotExist": "无效的边缘:{{node}} 的源/输出节点不存在",
"unableToExtractEnumOptions": "无法提取枚举选项",
"unableToParseFieldType": "无法解析类型",
"outputFieldInInput": "输入中的输出区域",
"unrecognizedWorkflowVersion": "无法识别的工作流架构版本:{{version}}",
"outputFieldTypeParseError": "无法解析 {{node}} 的输出类型 {{field}}。({{message}})",
"sourceNodeFieldDoesNotExist": "无效的边缘:{{node}} 的源/输出区域 {{field}} 不存在",
"unableToGetWorkflowVersion": "无法获取工作流架构版本",
"nodePack": "节点包",
"unableToExtractSchemaNameFromRef": "无法从参考中提取架构名",
"unableToMigrateWorkflow": "无法迁移工作流",
"unknownOutput": "未知输出:{{name}}",
"unableToUpdateNode": "无法更新节点",
"unknownErrorValidatingWorkflow": "验证工作流时出现未知错误",
"collectionFieldType": "{{name}} 合集",
"unknownNodeType": "未知节点类型",
"targetNodeDoesNotExist": "无效的边缘:{{node}} 的目标/输入节点不存在",
"unknownFieldType": "$t(nodes.unknownField) 类型:{{type}}",
"collectionOrScalarFieldType": "{{name}} 合集 | 标量",
"nodeVersion": "节点版本",
"deletedInvalidEdge": "已删除无效的边缘 {{source}} -> {{target}}",
"unknownInput": "未知输入:{{name}}"
},
"controlnet": {
"resize": "直接缩放",
@ -1245,7 +1297,8 @@
"fit": "图生图匹配",
"recallParameters": "召回参数",
"noRecallParameters": "未找到要召回的参数",
"vae": "VAE"
"vae": "VAE",
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)"
},
"models": {
"noMatchingModels": "无相匹配的模型",
@ -1258,7 +1311,8 @@
"noRefinerModelsInstalled": "无已安装的 SDXL Refiner 模型",
"noLoRAsInstalled": "无已安装的 LoRA",
"esrganModel": "ESRGAN 模型",
"addLora": "添加 LoRA"
"addLora": "添加 LoRA",
"noLoRAsLoaded": "无已加载的 LoRA"
},
"boards": {
"autoAddBoard": "自动添加面板",
@ -1280,12 +1334,14 @@
"deleteBoardOnly": "仅删除面板",
"deleteBoard": "删除面板",
"deleteBoardAndImages": "删除面板和图像",
"deletedBoardsCannotbeRestored": "已删除的面板无法被恢复"
"deletedBoardsCannotbeRestored": "已删除的面板无法被恢复",
"movingImagesToBoard_other": "移动 {{count}} 张图像到面板:"
},
"embedding": {
"noMatchingEmbedding": "不匹配的 Embedding",
"addEmbedding": "添加 Embedding",
"incompatibleModel": "不兼容的基础模型:"
"incompatibleModel": "不兼容的基础模型:",
"noEmbeddingsLoaded": "无已加载的 Embedding"
},
"dynamicPrompts": {
"seedBehaviour": {
@ -1514,6 +1570,12 @@
"ControlNet 为生成过程提供引导,为生成具有受控构图、结构、样式的图像提供帮助,具体的功能由所选的模型决定。"
],
"heading": "ControlNet"
},
"paramCFGRescaleMultiplier": {
"heading": "CFG 重缩放倍数",
"paragraphs": [
"CFG 引导的重缩放倍率,用于通过 zero-terminal SNR (ztsnr) 训练的模型。推荐设为 0.7。"
]
}
},
"invocationCache": {
@ -1530,7 +1592,8 @@
"enable": "启用",
"clear": "清除",
"maxCacheSize": "最大缓存大小",
"cacheSize": "缓存大小"
"cacheSize": "缓存大小",
"useCache": "使用缓存"
},
"hrf": {
"enableHrf": "启用高分辨率修复",

View File

@ -23,7 +23,8 @@ export type AppFeature =
| 'resumeQueue'
| 'prependQueue'
| 'invocationCache'
| 'bulkDownload';
| 'bulkDownload'
| 'workflowLibrary';
/**
* A disable-able Stable Diffusion feature

View File

@ -5,8 +5,12 @@ import UploadWorkflowButton from 'features/workflowLibrary/components/LoadWorkfl
import ResetWorkflowEditorButton from 'features/workflowLibrary/components/ResetWorkflowButton';
import SaveWorkflowButton from 'features/workflowLibrary/components/SaveWorkflowButton';
import SaveWorkflowAsButton from 'features/workflowLibrary/components/SaveWorkflowAsButton';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const TopCenterPanel = () => {
const isWorkflowLibraryEnabled =
useFeatureStatus('workflowLibrary').isFeatureEnabled;
return (
<Flex
sx={{
@ -19,8 +23,12 @@ const TopCenterPanel = () => {
>
<DownloadWorkflowButton />
<UploadWorkflowButton />
<SaveWorkflowButton />
<SaveWorkflowAsButton />
{isWorkflowLibraryEnabled && (
<>
<SaveWorkflowButton />
<SaveWorkflowAsButton />
</>
)}
<ResetWorkflowEditorButton />
</Flex>
);

View File

@ -2,11 +2,15 @@ import { Flex } from '@chakra-ui/react';
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
import { memo } from 'react';
import WorkflowEditorSettings from './WorkflowEditorSettings';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const TopRightPanel = () => {
const isWorkflowLibraryEnabled =
useFeatureStatus('workflowLibrary').isFeatureEnabled;
return (
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineEnd: 2 }}>
<WorkflowLibraryButton />
{isWorkflowLibraryEnabled && <WorkflowLibraryButton />}
<WorkflowEditorSettings />
</Flex>
);

View File

@ -1,4 +1,5 @@
import { Flex } from '@chakra-ui/react';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
import QueueControls from 'features/queue/components/QueueControls';
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
@ -11,7 +12,6 @@ import {
import 'reactflow/dist/style.css';
import InspectorPanel from './inspector/InspectorPanel';
import WorkflowPanel from './workflow/WorkflowPanel';
import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations';
const NodeEditorPanelGroup = () => {
const [isTopPanelCollapsed, setIsTopPanelCollapsed] = useState(false);
@ -22,7 +22,23 @@ const NodeEditorPanelGroup = () => {
if (!panelGroupRef.current) {
return;
}
panelGroupRef.current.setLayout([50, 50]);
panelGroupRef.current.setLayout([
{ sizePercentage: 50 },
{ sizePercentage: 50 },
]);
}, []);
const onCollapseTopPanel = useCallback(() => {
setIsTopPanelCollapsed(true);
}, []);
const onExpandTopPanel = useCallback(() => {
setIsTopPanelCollapsed(false);
}, []);
const onCollapseBottomPanel = useCallback(() => {
setIsBottomPanelCollapsed(true);
}, []);
const onExpandBottomPanel = useCallback(() => {
setIsBottomPanelCollapsed(false);
}, []);
return (
@ -53,8 +69,9 @@ const NodeEditorPanelGroup = () => {
<Panel
id="workflow"
collapsible
onCollapse={setIsTopPanelCollapsed}
minSize={25}
onCollapse={onCollapseTopPanel}
onExpand={onExpandTopPanel}
minSizePercentage={25}
>
<WorkflowPanel />
</Panel>
@ -72,8 +89,9 @@ const NodeEditorPanelGroup = () => {
<Panel
id="inspector"
collapsible
onCollapse={setIsBottomPanelCollapsed}
minSize={25}
onCollapse={onCollapseBottomPanel}
onExpand={onExpandBottomPanel}
minSizePercentage={25}
>
<InspectorPanel />
</Panel>

View File

@ -14,7 +14,13 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import { InvokeTabName } from 'features/ui/store/tabMap';
import {
activeTabIndexSelector,
activeTabNameSelector,
} from 'features/ui/store/uiSelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { ResourceKey } from 'i18next';
import { isEqual } from 'lodash-es';
@ -25,12 +31,6 @@ import { FaCube, FaFont, FaImage, FaStream } from 'react-icons/fa';
import { FaCircleNodes } from 'react-icons/fa6';
import { MdGridOn } from 'react-icons/md';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
import {
activeTabIndexSelector,
activeTabNameSelector,
} from 'features/ui/store/uiSelectors';
import FloatingGalleryButton from './FloatingGalleryButton';
import FloatingSidePanelButtons from './FloatingParametersPanelButtons';
import ParametersPanel from './ParametersPanel';
@ -160,31 +160,28 @@ const InvokeTabs = () => {
);
const {
minSize: sidePanelMinSize,
isCollapsed: isSidePanelCollapsed,
setIsCollapsed: setIsSidePanelCollapsed,
ref: sidePanelRef,
reset: resetSidePanel,
expand: expandSidePanel,
collapse: collapseSidePanel,
toggle: toggleSidePanel,
} = usePanel(SIDE_PANEL_MIN_SIZE_PX, 'pixels');
} = usePanel({ sizePixels: SIDE_PANEL_MIN_SIZE_PX });
const {
ref: galleryPanelRef,
minSize: galleryPanelMinSize,
isCollapsed: isGalleryPanelCollapsed,
setIsCollapsed: setIsGalleryPanelCollapsed,
reset: resetGalleryPanel,
expand: expandGalleryPanel,
collapse: collapseGalleryPanel,
toggle: toggleGalleryPanel,
} = usePanel(GALLERY_PANEL_MIN_SIZE_PX, 'pixels');
} = usePanel({ sizePixels: GALLERY_PANEL_MIN_SIZE_PX });
useHotkeys(
'f',
() => {
if (isGalleryPanelCollapsed || isSidePanelCollapsed) {
if (
sidePanelRef.current?.isCollapsed() ||
galleryPanelRef.current?.isCollapsed()
) {
expandGalleryPanel();
expandSidePanel();
} else {
@ -192,7 +189,7 @@ const InvokeTabs = () => {
collapseGalleryPanel();
}
},
[dispatch, isGalleryPanelCollapsed, isSidePanelCollapsed]
[dispatch, sidePanelRef, galleryPanelRef]
);
useHotkeys(
@ -213,6 +210,22 @@ const InvokeTabs = () => {
const panelStorage = usePanelStorage();
console.log({
sidePanelRef,
resetSidePanel,
expandSidePanel,
collapseSidePanel,
toggleSidePanel,
});
console.log({
galleryPanelRef,
resetGalleryPanel,
expandGalleryPanel,
collapseGalleryPanel,
toggleGalleryPanel,
});
return (
<Tabs
variant="appTabs"
@ -241,7 +254,6 @@ const InvokeTabs = () => {
direction="horizontal"
style={{ height: '100%', width: '100%' }}
storage={panelStorage}
units="pixels"
>
{!NO_SIDE_PANEL_TABS.includes(activeTabName) && (
<>
@ -249,9 +261,8 @@ const InvokeTabs = () => {
order={0}
id="side"
ref={sidePanelRef}
defaultSize={sidePanelMinSize}
minSize={sidePanelMinSize}
onCollapse={setIsSidePanelCollapsed}
defaultSizePixels={SIDE_PANEL_MIN_SIZE_PX}
minSizePixels={SIDE_PANEL_MIN_SIZE_PX}
collapsible
>
{activeTabName === 'nodes' ? (
@ -262,15 +273,19 @@ const InvokeTabs = () => {
</Panel>
<ResizeHandle
onDoubleClick={resetSidePanel}
collapsedDirection={isSidePanelCollapsed ? 'left' : undefined}
collapsedDirection={
sidePanelRef.current?.isCollapsed() ? 'left' : undefined
}
/>
<FloatingSidePanelButtons
isSidePanelCollapsed={isSidePanelCollapsed}
isSidePanelCollapsed={Boolean(
sidePanelRef.current?.isCollapsed()
)}
sidePanelRef={sidePanelRef}
/>
</>
)}
<Panel id="main" order={1} minSize={MAIN_PANEL_MIN_SIZE_PX}>
<Panel id="main" order={1} minSizePixels={MAIN_PANEL_MIN_SIZE_PX}>
<TabPanels style={{ height: '100%', width: '100%' }}>
{tabPanels}
</TabPanels>
@ -279,21 +294,24 @@ const InvokeTabs = () => {
<>
<ResizeHandle
onDoubleClick={resetGalleryPanel}
collapsedDirection={isGalleryPanelCollapsed ? 'right' : undefined}
collapsedDirection={
galleryPanelRef.current?.isCollapsed() ? 'right' : undefined
}
/>
<Panel
id="gallery"
ref={galleryPanelRef}
order={2}
defaultSize={galleryPanelMinSize}
minSize={galleryPanelMinSize}
onCollapse={setIsGalleryPanelCollapsed}
defaultSizePixels={GALLERY_PANEL_MIN_SIZE_PX}
minSizePixels={GALLERY_PANEL_MIN_SIZE_PX}
collapsible
>
<ImageGalleryContent />
</Panel>
<FloatingGalleryButton
isGalleryCollapsed={isGalleryPanelCollapsed}
isGalleryCollapsed={Boolean(
galleryPanelRef.current?.isCollapsed()
)}
galleryPanelRef={galleryPanelRef}
/>
</>

View File

@ -17,7 +17,10 @@ const ImageToImageTab = () => {
if (!panelGroupRef.current) {
return;
}
panelGroupRef.current.setLayout([50, 50]);
panelGroupRef.current.setLayout([
{ sizePercentage: 50 },
{ sizePercentage: 50 },
]);
}, []);
const panelStorage = usePanelStorage();
@ -30,13 +33,12 @@ const ImageToImageTab = () => {
direction="horizontal"
style={{ height: '100%', width: '100%' }}
storage={panelStorage}
units="percentages"
>
<Panel
id="imageTab.content.initImage"
order={0}
defaultSize={50}
minSize={25}
defaultSizePercentage={50}
minSizePercentage={25}
style={{ position: 'relative' }}
>
<InitialImageDisplay />
@ -45,8 +47,8 @@ const ImageToImageTab = () => {
<Panel
id="imageTab.content.selectedImage"
order={1}
defaultSize={50}
minSize={25}
defaultSizePercentage={50}
minSizePercentage={25}
>
<TextToImageTabMain />
</Panel>

View File

@ -1,16 +1,12 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useRef } from 'react';
import { flushSync } from 'react-dom';
import { ImperativePanelHandle, Units } from 'react-resizable-panels';
import { ImperativePanelHandle, MixedSizes } from 'react-resizable-panels';
export const usePanel = (minSize: number, units: Units) => {
export const usePanel = (minSize: Partial<MixedSizes>) => {
const ref = useRef<ImperativePanelHandle>(null);
const [isCollapsed, setIsCollapsed] = useState(() =>
Boolean(ref.current?.getCollapsed())
);
const toggle = useCallback(() => {
if (ref.current?.getCollapsed()) {
if (ref.current?.isCollapsed()) {
flushSync(() => {
ref.current?.expand();
});
@ -35,15 +31,12 @@ export const usePanel = (minSize: number, units: Units) => {
const reset = useCallback(() => {
flushSync(() => {
ref.current?.resize(minSize, units);
ref.current?.resize(minSize);
});
}, [minSize, units]);
}, [minSize]);
return {
ref,
minSize,
isCollapsed,
setIsCollapsed,
reset,
toggle,
expand,

View File

@ -5496,10 +5496,10 @@ react-remove-scroll@^2.5.5, react-remove-scroll@^2.5.6:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-resizable-panels@^0.0.55:
version "0.0.55"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-0.0.55.tgz#adf06d35ae09748ab7051a4bd2c5be8087ef1a66"
integrity sha512-J/LTFzUEjJiqwSjVh8gjUXkQDA8MRPjARASfn++d2+KOgA+9UcRYUfE3QBJixer2vkk+ffQ4cq3QzWzzHgqYpQ==
react-resizable-panels@^0.0.63:
version "0.0.63"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-0.0.63.tgz#d364d84ee5927bfb0e56c7ea75eb6504e9041a21"
integrity sha512-AfA8b6kouhL4rBvgUGs17uzWVlYPaJIwwTCVeWRxNpUHJlCG1h9RIMlzA1849AZGsaNJO3j/SNdI5SS4GZDE3g==
react-style-singleton@^2.2.1:
version "2.2.1"

View File

@ -0,0 +1,57 @@
"""
Test interaction of logging with configuration system.
"""
import io
import logging
import re
from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.backend.util.logging import LOG_FORMATTERS, InvokeAILogger
# test formatting
# Would prefer to use the capfd/capsys fixture here, but it is broken
# when used with the logging module: https://github.com/pytest-dev/pytest/issue
def test_formatting():
logger = InvokeAILogger.get_logger()
stream = io.StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(LOG_FORMATTERS["plain"]())
logger.addHandler(handler)
logger.info("test1")
output = stream.getvalue()
assert re.search(r"\[InvokeAI\]::INFO --> test1$", output)
handler.setFormatter(LOG_FORMATTERS["legacy"]())
logger.info("test2")
output = stream.getvalue()
assert re.search(r">> test2$", output)
# test independence of two loggers with different names
def test_independence():
logger1 = InvokeAILogger.get_logger()
logger2 = InvokeAILogger.get_logger("Test")
assert logger1.name == "InvokeAI"
assert logger2.name == "Test"
assert logger1.level == logging.INFO
assert logger2.level == logging.INFO
logger2.setLevel(logging.DEBUG)
assert logger1.level == logging.INFO
assert logger2.level == logging.DEBUG
# test that the logger is returned from two similar get_logger() calls
def test_retrieval():
logger1 = InvokeAILogger.get_logger()
logger2 = InvokeAILogger.get_logger()
logger3 = InvokeAILogger.get_logger("Test")
assert logger1 == logger2
assert logger1 != logger3
# test that the configuration is used to set the initial logging level
def test_config():
config = InvokeAIAppConfig(log_level="debug")
logger1 = InvokeAILogger.get_logger("DebugTest", config=config)
assert logger1.level == logging.DEBUG