Skip to content

Commit

Permalink
Update st.spinner (streamlit#7488)
Browse files Browse the repository at this point in the history
Update st.spinner - special overlay spinner for caching functions & updated styling for normal spinner
  • Loading branch information
mayagbarnes authored Oct 11, 2023
1 parent 3974c4d commit 46c8f61
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 26 deletions.
8 changes: 8 additions & 0 deletions frontend/lib/src/components/core/Block/styled-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ export const StyledElementContainer = styled.div<StyledElementContainerProps>(
overflow: "visible",
},

":has(> .cacheSpinner)": {
height: 0,
overflow: "visible",
visibility: "visible",
marginBottom: "-1rem",
zIndex: 1000,
},

// We do not want the chat input to be faded out.
// TODO: Reconsider this when we implement fixed-sized chat containers
...(isStale && elementType !== "chatInput"
Expand Down
19 changes: 18 additions & 1 deletion frontend/lib/src/components/elements/Spinner/Spinner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import { render } from "@streamlit/lib/src/test_util"
import Spinner, { SpinnerProps } from "./Spinner"

const getProps = (
propOverrides: Partial<SpinnerProps> = {}
propOverrides: Partial<SpinnerProps> = {},
elementOverrides: Partial<SpinnerProto> = {}
): SpinnerProps => ({
element: SpinnerProto.create({
text: "Loading...",
...elementOverrides,
}),
width: 0,
...propOverrides,
Expand Down Expand Up @@ -61,4 +63,19 @@ describe("Spinner component", () => {
const spinnerElement = screen.getByTestId("stSpinner")
expect(spinnerElement).toHaveStyle(`width: 100px`)
})

it("sets additional className/CSS for caching spinner", () => {
render(
<BaseProvider theme={LightTheme}>
<Spinner {...getProps({}, { cache: true })} />
</BaseProvider>
)

const spinnerContainer = screen.getByTestId("stSpinner")
expect(spinnerContainer).toBeInTheDocument()

expect(spinnerContainer).toHaveClass("stSpinner")
expect(spinnerContainer).toHaveClass("cacheSpinner")
expect(spinnerContainer).toHaveStyle("paddingBottom: 1rem")
})
})
23 changes: 13 additions & 10 deletions frontend/lib/src/components/elements/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
*/

import React, { ReactElement } from "react"
import { useTheme } from "@emotion/react"
import { EmotionTheme, isPresetTheme } from "@streamlit/lib/src/theme"
import classNames from "classnames"
import { isPresetTheme } from "@streamlit/lib/src/theme"
import { Spinner as SpinnerProto } from "@streamlit/lib/src/proto"
import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown"
import { LibContext } from "@streamlit/lib/src/components/core/LibContext"

import {
StyledSpinner,
StyledSpinnerContainer,
ThemedStyledSpinner,
} from "./styled-components"
Expand All @@ -31,21 +33,22 @@ export interface SpinnerProps {
}

function Spinner({ width, element }: SpinnerProps): ReactElement {
const theme: EmotionTheme = useTheme()
const { activeTheme } = React.useContext(LibContext)
const usingCustomTheme = !isPresetTheme(activeTheme)
const styleProp = { width }
const { cache } = element

return (
<div className="stSpinner" data-testid="stSpinner" style={styleProp}>
<StyledSpinner
className={classNames({ stSpinner: true, cacheSpinner: cache })}
data-testid="stSpinner"
width={width}
cache={cache}
>
<StyledSpinnerContainer>
<ThemedStyledSpinner
$size={theme.iconSizes.twoXL}
$usingCustomTheme={usingCustomTheme}
/>
<ThemedStyledSpinner usingCustomTheme={usingCustomTheme} />
<StreamlitMarkdown source={element.text} allowHTML={false} />
</StyledSpinnerContainer>
</div>
</StyledSpinner>
)
}

Expand Down
38 changes: 31 additions & 7 deletions frontend/lib/src/components/elements/Spinner/styled-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,51 @@
import styled from "@emotion/styled"
import { Spinner } from "baseui/spinner"
import isPropValid from "@emotion/is-prop-valid"
interface ThemedStyledSpinnerProps {
usingCustomTheme: boolean
}

export const ThemedStyledSpinner = styled(Spinner, {
shouldForwardProp: isPropValid,
})(({ theme, $usingCustomTheme }) => {
})<ThemedStyledSpinnerProps>(({ theme, usingCustomTheme }) => {
return {
marginTop: theme.spacing.none,
marginBottom: theme.spacing.none,
marginRight: theme.spacing.none,
marginLeft: theme.spacing.none,
fontSize: theme.fontSizes.sm,
width: "1.375rem",
height: "1.375rem",
borderWidth: "3px",
radius: "4px",
justifyContents: "center",
padding: theme.spacing.none,
margin: theme.spacing.none,
borderColor: theme.colors.fadedText10,
borderTopColor: $usingCustomTheme
borderTopColor: usingCustomTheme
? theme.colors.primary
: theme.colors.blue70,
flexGrow: 0,
flexShrink: 0,
}
})

interface StyledSpinnerProps {
width: number
cache: boolean
}

export const StyledSpinner = styled.div<StyledSpinnerProps>(
({ theme, width, cache }) => ({
width: width,
...(cache
? {
paddingBottom: "1rem",
background: `linear-gradient(to bottom, ${theme.colors.bgColor} 0%, ${theme.colors.bgColor} 80%, transparent 100%)`,
}
: null),
})
)

export const StyledSpinnerContainer = styled.div(({ theme }) => ({
display: "flex",
gap: theme.spacing.lg,
gap: theme.spacing.sm,
alignItems: "center",
width: "100%",
}))
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export interface Props {
disableLinks?: boolean

/**
* Toast has smaller font sizing
* Toast has smaller font sizing & special CSS
*/
isToast?: boolean
}
Expand Down
7 changes: 4 additions & 3 deletions lib/streamlit/elements/spinner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@


@contextlib.contextmanager
def spinner(text: str = "In progress...") -> Iterator[None]:
def spinner(text: str = "In progress...", *, cache: bool = False) -> Iterator[None]:
"""Temporarily displays a message while executing a block of code.
Parameters
Expand Down Expand Up @@ -55,9 +55,9 @@ def spinner(text: str = "In progress...") -> Iterator[None]:
with caching.suppress_cached_st_function_warning():
message = st.empty()

# Set the message 0.1 seconds in the future to avoid annoying
# Set the message 0.5 seconds in the future to avoid annoying
# flickering if this spinner runs too quickly.
DELAY_SECS = 0.1
DELAY_SECS = 0.5
display_message = True
display_message_lock = threading.Lock()

Expand All @@ -70,6 +70,7 @@ def set_message():
with caching.suppress_cached_st_function_warning():
spinner_proto = SpinnerProto()
spinner_proto.text = clean_text(text)
spinner_proto.cache = cache
message._enqueue("spinner", spinner_proto)

add_script_run_ctx(threading.Timer(DELAY_SECS, set_message)).start()
Expand Down
2 changes: 1 addition & 1 deletion lib/streamlit/runtime/caching/cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def __call__(self, *args, **kwargs) -> Any:
message = self._info.show_spinner

if self._info.show_spinner or isinstance(self._info.show_spinner, str):
with spinner(message):
with spinner(message, cache=True):
return self._get_or_create_cached_value(args, kwargs)
else:
return self._get_or_create_cached_value(args, kwargs)
Expand Down
2 changes: 1 addition & 1 deletion lib/streamlit/runtime/legacy_caching/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ def get_or_create_cached_value():
return return_value

if show_spinner:
with spinner(message):
with spinner(message, cache=True):
return get_or_create_cached_value()
else:
return get_or_create_cached_value()
Expand Down
19 changes: 17 additions & 2 deletions lib/tests/streamlit/spinner_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ def test_spinner(self):
"""Test st.spinner."""
with spinner("some text"):
# Without the timeout, the spinner is sometimes not available
time.sleep(0.2)
time.sleep(0.7)
el = self.get_delta_from_queue().new_element
self.assertEqual(el.spinner.text, "some text")
self.assertFalse(el.spinner.cache)
# Check if it gets reset to st.empty()
last_delta = self.get_delta_from_queue()
self.assertTrue(last_delta.HasField("new_element"))
Expand All @@ -38,9 +39,10 @@ def test_spinner_within_chat_message(self):
with st.chat_message("user"):
with spinner("some text"):
# Without the timeout, the spinner is sometimes not available
time.sleep(0.2)
time.sleep(0.7)
el = self.get_delta_from_queue().new_element
self.assertEqual(el.spinner.text, "some text")
self.assertFalse(el.spinner.cache)
# Check that the element gets reset to an empty container block:
last_delta = self.get_delta_from_queue()
self.assertTrue(last_delta.HasField("add_block"))
Expand All @@ -49,3 +51,16 @@ def test_spinner_within_chat_message(self):
# it the container is empty. This is the desired behavior
# for spinner
self.assertFalse(last_delta.add_block.allow_empty)

def test_spinner_for_caching(self):
"""Test st.spinner in cache functions."""
with spinner("some text", cache=True):
# Without the timeout, the spinner is sometimes not available
time.sleep(0.7)
el = self.get_delta_from_queue().new_element
self.assertEqual(el.spinner.text, "some text")
self.assertTrue(el.spinner.cache)
# Check if it gets reset to st.empty()
last_delta = self.get_delta_from_queue()
self.assertTrue(last_delta.HasField("new_element"))
self.assertEqual(last_delta.new_element.WhichOneof("type"), "empty")
3 changes: 3 additions & 0 deletions proto/streamlit/proto/Spinner.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ syntax = "proto3";
message Spinner {
// A message to display while executing that block.
string text = 1;

// Whether spinner used in caching functions.
bool cache = 2;
}

0 comments on commit 46c8f61

Please sign in to comment.