/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/app/src/components/StatusWidget/StatusWidget.tsx
327 строк
9 KB
dependabot[bot]
Bump the eslint group in /frontend with 7 updates (#13502)
05 янв 2026, 20:23
Не верифицирован
05 янв 2026, 20:23
9c391de
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { ReactElement, ReactNode, useCallback, useEffect, useRef, useState, } from "react" import Hotkeys from "react-hot-keys" import { CSSTransition } from "react-transition-group" import { ConnectionState } from "@streamlit/connection" import { BaseButton, BaseButtonKind, DynamicIcon, Icon, isKeyboardEventFromEditableTarget, Placement, ScriptRunState, Timer, Tooltip, useEmotionTheme, } from "@streamlit/lib" import { isNullOrUndefined, notNullOrUndefined } from "@streamlit/utils" import { getConnectionStateUI } from "./getConnectionStateUI" import IconRunning from "./IconRunning" import { StyledAppButtonContainer, StyledAppStatus, StyledAppStatusLabel, StyledConnectionStatus, StyledConnectionStatusLabel, StyledShortcutLabel, StyledStatusWidget, } from "./styled-components" /** Component props */ export interface StatusWidgetProps { /** State of our connection to the server. */ connectionState: ConnectionState /** Script's current run state */ scriptRunState: ScriptRunState /** * Function called when the user chooses to re-run a script in response to * its source file changing. * * @param alwaysRerun if true, also change the run-on-save setting for this * session */ rerunScript: (alwaysRerun: boolean) => void /** Function called when the user chooses to stop the running script. */ stopScript: () => void /** Allows users to change user settings to allow rerun on save */ allowRunOnSave: boolean /** Whether to show script changed actions (rerun/always rerun buttons) */ showScriptChangedActions: boolean } // Delay time for displaying running man animation. const RUNNING_MAN_DISPLAY_DELAY_TIME_MS = 500 interface PromptButtonProps { title: ReactNode disabled: boolean onClick: () => void } const PromptButton = (props: PromptButtonProps): ReactElement => { return ( <StyledAppButtonContainer> <BaseButton kind={BaseButtonKind.HEADER_BUTTON} disabled={props.disabled} containerWidth onClick={props.onClick} > {props.title} </BaseButton> </StyledAppButtonContainer> ) } /** * Displays various script- and connection-related info: our WebSocket * connection status, the run-state of our script, and other transient events. */ const StatusWidget: React.FC<StatusWidgetProps> = ({ connectionState, scriptRunState, rerunScript, stopScript, allowRunOnSave, showScriptChangedActions, }) => { const [showRunningMan, setShowRunningMan] = useState(false) const minimizePromptTimerRef: React.MutableRefObject<Timer | null> = useRef(null) const delayShowRunningManTimerRef: React.MutableRefObject<Timer | null> = useRef(null) const theme = useEmotionTheme() const handleAlwaysRerunClick = (): void => { if (allowRunOnSave) { rerunScript(true) } } const handleKeyDown = ( keyName: string, keyboardEvent?: KeyboardEvent ): void => { // NOTE: 'r' and 'c' are handled at the App level. // See `isKeyboardEventFromEditableTarget` for editable/shadow DOM behavior; // we suppress the "Always rerun" hotkey while the user is typing. if (keyName === "a" && !isKeyboardEventFromEditableTarget(keyboardEvent)) { handleAlwaysRerunClick() } } const isConnected = connectionState === ConnectionState.CONNECTED const showRunningManAfterInitialDelay = useCallback( (delay: number): void => { if (delayShowRunningManTimerRef.current !== null) { delayShowRunningManTimerRef.current.setTimeout(() => { setShowRunningMan(true) }, delay) } }, [] ) const handleStopScriptClick = (): void => { stopScript() } const handleRerunClick = (): void => { rerunScript(false) } useEffect(() => { if (minimizePromptTimerRef.current === null) { minimizePromptTimerRef.current = new Timer() } if (delayShowRunningManTimerRef.current === null) { delayShowRunningManTimerRef.current = new Timer() } const minimizePromptTimerCurr = minimizePromptTimerRef.current const delayShowRunningManTimerCurr = minimizePromptTimerRef.current return () => { minimizePromptTimerCurr.cancel() delayShowRunningManTimerCurr.cancel() } }, []) useEffect(() => { if (isConnected) { if ( scriptRunState === ScriptRunState.RUNNING || scriptRunState === ScriptRunState.RERUN_REQUESTED ) { showRunningManAfterInitialDelay(RUNNING_MAN_DISPLAY_DELAY_TIME_MS) } } if (scriptRunState === ScriptRunState.NOT_RUNNING) { setShowRunningMan(false) } }, [scriptRunState, showRunningManAfterInitialDelay, isConnected]) const renderScriptIsRunning = (): ReactNode => { const stopRequested = scriptRunState === ScriptRunState.STOP_REQUESTED return showRunningMan ? ( <StyledAppStatus> <IconRunning /> <PromptButton title={stopRequested ? "Stopping..." : "Stop"} disabled={stopRequested} onClick={handleStopScriptClick} /> </StyledAppStatus> ) : ( <></> ) } const renderRerunScriptPrompt = (): ReactNode => { const rerunRequested = scriptRunState === ScriptRunState.RERUN_REQUESTED return ( <Hotkeys keyName="a" onKeyDown={handleKeyDown}> <StyledAppStatus> <DynamicIcon size="lg" iconValue=":material/info:" color={theme.colors.fadedText60} /> <StyledAppStatusLabel isPrompt>File change.</StyledAppStatusLabel> <PromptButton title={<StyledShortcutLabel>Rerun</StyledShortcutLabel>} disabled={rerunRequested} onClick={handleRerunClick} /> {allowRunOnSave && ( <PromptButton title={<StyledShortcutLabel>Always rerun</StyledShortcutLabel>} disabled={rerunRequested} onClick={handleAlwaysRerunClick} /> )} </StyledAppStatus> </Hotkeys> ) } const renderConnectionStatus = (): ReactNode => { const ui = getConnectionStateUI(connectionState) if (ui === undefined) { return null } return ( <Tooltip content={ui.tooltip} placement={Placement.BOTTOM}> <StyledConnectionStatus className="stConnectionStatus" data-testid="stConnectionStatus" > <Icon size="sm" content={ui.icon} /> <StyledConnectionStatusLabel>{ui.label}</StyledConnectionStatusLabel> </StyledConnectionStatus> </Tooltip> ) } const renderWidget = (): ReactNode => { if (isConnected) { if ( scriptRunState === ScriptRunState.RUNNING || scriptRunState === ScriptRunState.RERUN_REQUESTED ) { // Show scriptIsRunning when the script is actually running, // but also when the user has just requested a re-run. // In the latter case, the server should get around to actually // re-running the script in a second or two, but we can appear // more responsive by claiming it's started immediately. return renderScriptIsRunning() } if (showScriptChangedActions) { return renderRerunScriptPrompt() } } return renderConnectionStatus() } // The StatusWidget fades in on appear and fades out on disappear. // We keep track of our most recent result from `renderWidget`, // via `this.curView`, so that we can fade out our previous state // if `renderWidget` returns null after returning a non-null value. const curViewRef = useRef<ReactNode>() // eslint-disable-next-line react-hooks/refs -- TODO: Do not access ref during render const prevView = curViewRef.current // eslint-disable-next-line react-hooks/refs -- TODO: Do not access ref during render curViewRef.current = renderWidget() // eslint-disable-next-line react-hooks/refs -- TODO: Do not access ref during render if (isNullOrUndefined(curViewRef.current) && isNullOrUndefined(prevView)) { return <></> } let animateIn: boolean let renderView: ReactNode // eslint-disable-next-line react-hooks/refs -- TODO: Do not access ref during render if (notNullOrUndefined(curViewRef.current)) { animateIn = true // eslint-disable-next-line react-hooks/refs -- TODO: Do not access ref during render renderView = curViewRef.current } else { animateIn = false renderView = prevView } // NB: the `timeout` value here must match the transition // times specified in the StatusWidget-*-active CSS classes return ( <CSSTransition appear={true} in={animateIn} timeout={200} unmountOnExit={true} classNames="StatusWidget" > <StyledStatusWidget key="StatusWidget" className="stStatusWidget" data-testid="stStatusWidget" > {renderView} </StyledStatusWidget> </CSSTransition> ) } export default StatusWidget