Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add st.status container #7140

Merged
merged 43 commits into from Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f6e89d0
Add icon to expander
LukasMasuch Aug 8, 2023
de7df4c
Add implementation for st.status
LukasMasuch Aug 9, 2023
96d0eff
Fix
LukasMasuch Aug 9, 2023
92c604a
Add top comment
LukasMasuch Aug 9, 2023
e95a6e0
Add docstring
LukasMasuch Aug 9, 2023
739cbd8
Fixes
LukasMasuch Aug 9, 2023
63bbfee
Improve implementation
LukasMasuch Aug 9, 2023
611bf85
Some refactoring
LukasMasuch Aug 9, 2023
52429b0
Merge remote-tracking branch 'remote/develop' into feature/mutable-ex…
LukasMasuch Aug 14, 2023
bbdfab5
Minor fixes
LukasMasuch Aug 14, 2023
12cc34c
Add auto collapse mode
LukasMasuch Aug 14, 2023
51b4810
Add auto collapse mode
LukasMasuch Aug 14, 2023
cb86aeb
Various fixes and improvements
LukasMasuch Aug 14, 2023
7729071
Add empty expander test
LukasMasuch Aug 14, 2023
3303b55
Add empty expander test
LukasMasuch Aug 14, 2023
ac7b3de
Disallow empty for normal expander
LukasMasuch Aug 15, 2023
b5b7cd9
Apply various updates
LukasMasuch Aug 15, 2023
1fe687d
Some refactoring
LukasMasuch Aug 15, 2023
4a440b7
Update status container
LukasMasuch Aug 15, 2023
286cc9a
Update callback handler
LukasMasuch Aug 15, 2023
f0bab52
Change empty state for expander
LukasMasuch Aug 15, 2023
4f37986
Move icon implementation to expander
LukasMasuch Aug 15, 2023
5e4f6a1
Add e2e tests
LukasMasuch Aug 15, 2023
3917a88
Add unit tests
LukasMasuch Aug 15, 2023
b16e912
Add empty expander e2e test
LukasMasuch Aug 15, 2023
b8aece9
Update comment
LukasMasuch Aug 15, 2023
410203a
Cleanups
LukasMasuch Aug 15, 2023
3553d4c
Cleanup
LukasMasuch Aug 15, 2023
c0c308e
Add metrics
LukasMasuch Aug 15, 2023
dd3c38e
Update docstring
LukasMasuch Aug 15, 2023
a29dbab
Update snapshot
LukasMasuch Aug 15, 2023
8875692
Add test for empty status
LukasMasuch Aug 15, 2023
89cc355
Use smaller block
LukasMasuch Aug 15, 2023
54aa8a9
Update docstrings
LukasMasuch Aug 15, 2023
dd2a969
Don't change color on hover if empty
LukasMasuch Aug 15, 2023
e58ed16
Update docstrings based on feedback
LukasMasuch Aug 15, 2023
5f23758
Minor change
LukasMasuch Aug 15, 2023
c2fa0b2
Update playback tests
LukasMasuch Aug 15, 2023
c6f94a5
Integrated PR feedback
LukasMasuch Aug 16, 2023
85cdae5
Fix test
LukasMasuch Aug 16, 2023
1830d78
Use descriptive names for screenshot tests
LukasMasuch Aug 17, 2023
8a176e5
Update status snapshots
LukasMasuch Aug 17, 2023
035ebf4
Update st.status docstring
sfc-gh-dmatthews Aug 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions e2e/playwright/st_expander.py
Expand Up @@ -24,3 +24,5 @@

sidebar = st.sidebar.expander("Expand me!")
sidebar.write("I am in the sidebar")

st.expander("Empty expander")
5 changes: 5 additions & 0 deletions e2e/playwright/st_expander_test.py
Expand Up @@ -70,3 +70,8 @@ def test_expander_collapses_and_expands(app: Page):
expander_header.click()
toggle = expander_header.locator("svg").first
expect(toggle).to_be_visible()


def test_empty_expander_not_rendered(app: Page):
"""Test that an empty expander is not rendered."""
expect(app.get_by_text("Empty expander")).not_to_be_attached()
48 changes: 48 additions & 0 deletions e2e/playwright/st_status.py
@@ -0,0 +1,48 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
#
# 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 streamlit as st

running_status = st.status("Running status", expanded=True)
running_status.write("Doing some work...")

with st.status("Completed status", expanded=True, state="complete"):
st.write("Hello world")

with st.status("Error status", expanded=True, state="error"):
st.error("Oh no, something went wrong!")

with st.status("Collapsed", state="complete"):
st.write("Hello world")

with st.status("About to change label...", state="complete") as status:
st.write("Hello world")
status.update(label="Changed label")

status = st.status("Without context manager", state="complete")
status.write("Hello world")
status.update(state="error", expanded=True)

with st.status("Collapse via update...", state="complete", expanded=True) as status:
st.write("Hello world")
status.update(label="Collapsed", expanded=False)

st.status("Empty state...", state="complete")

try:
with st.status("Uncaught exception"):
st.write("Hello world")
raise Exception("Error!")
except Exception:
pass
54 changes: 54 additions & 0 deletions e2e/playwright/st_status_test.py
@@ -0,0 +1,54 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
#
# 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.


from playwright.sync_api import Page, expect

from conftest import ImageCompareFunction


def test_status_container_rendering(
themed_app: Page, assert_snapshot: ImageCompareFunction
):
"""Test that st.status renders correctly via screenshots."""
status_containers = themed_app.get_by_test_id("stExpander")
expect(status_containers).to_have_count(9)

# Don't check screenshot for first element,
# since we cannot reliably screenshot test the spinner icon.
for i, element in enumerate(status_containers.all()[1:]):
assert_snapshot(element, name=f"status-{i}")
LukasMasuch marked this conversation as resolved.
Show resolved Hide resolved


def test_running_state(app: Page):
"""Test that st.status renders a spinner when in running state."""
running_status = app.get_by_test_id("stExpander").nth(0)
# Check if it has a spinner icon:
expect(running_status.get_by_test_id("stExpanderIconSpinner")).to_be_visible()


def test_status_collapses_and_expands(app: Page):
"""Test that a status collapses and expands."""
expander_content = "Doing some work..."
running_status = app.get_by_test_id("stExpander").nth(0)
# Starts expanded:
expect(running_status.get_by_text(expander_content)).to_be_visible()

expander_header = running_status.locator(".streamlit-expanderHeader")
# Collapse:
expander_header.click()
expect(running_status.get_by_text(expander_content)).not_to_be_attached()
# Expand:
expander_header.click()
expect(running_status.get_by_text(expander_content)).to_be_visible()
8 changes: 1 addition & 7 deletions frontend/lib/src/components/core/Block/Block.tsx
Expand Up @@ -52,13 +52,7 @@ interface BlockPropsWithWidth extends BaseBlockProps {
const BlockNodeRenderer = (props: BlockPropsWithWidth): ReactElement => {
const { node } = props

// Allow columns and chat messages to create the specified space regardless of empty state
// TODO: Maybe we can simplify this to: node.isEmpty && !node.deltaBlock.allowEmpty?
if (
node.isEmpty &&
!node.deltaBlock.column &&
!node.deltaBlock.chatMessage
) {
if (node.isEmpty && !node.deltaBlock.allowEmpty) {
return <></>
}

Expand Down
38 changes: 38 additions & 0 deletions frontend/lib/src/components/elements/Expander/Expander.test.tsx
Expand Up @@ -61,6 +61,44 @@ describe("Expander container", () => {
expect(screen.getByText(props.element.label)).toBeInTheDocument()
})

it("renders no collapse/expand icon if empty", () => {
const props = getProps({}, { empty: true })
render(<Expander {...props}></Expander>)
expect(
screen.queryByTestId("stExpanderToggleIcon")
).not.toBeInTheDocument()
})

it("renders expander with a spinner icon", () => {
const props = getProps({ icon: "spinner" })
render(
<Expander {...props}>
<div>test</div>
</Expander>
)
expect(screen.getByTestId("stExpanderIconSpinner")).toBeInTheDocument()
})

it("renders expander with a check icon", () => {
const props = getProps({ icon: "check" })
render(
<Expander {...props}>
<div>test</div>
</Expander>
)
expect(screen.getByTestId("stExpanderIconCheck")).toBeInTheDocument()
})

it("renders expander with a error icon", () => {
const props = getProps({ icon: "error" })
render(
<Expander {...props}>
<div>test</div>
</Expander>
)
expect(screen.getByTestId("stExpanderIconError")).toBeInTheDocument()
})

it("should render a expanded component", () => {
const props = getProps()
render(
Expand Down
128 changes: 111 additions & 17 deletions frontend/lib/src/components/elements/Expander/Expander.tsx
Expand Up @@ -22,13 +22,85 @@ import {
SharedStylePropsArg,
} from "baseui/accordion"
import { useTheme } from "@emotion/react"
import { ExpandMore, ExpandLess } from "@emotion-icons/material-outlined"
import { Block as BlockProto } from "@streamlit/lib/src/proto"
import {
ExpandMore,
ExpandLess,
Check,
ErrorOutline,
} from "@emotion-icons/material-outlined"

import Icon from "@streamlit/lib/src/components/shared/Icon"
import { Block as BlockProto } from "@streamlit/lib/src/proto"
import {
StyledSpinnerIcon,
StyledIcon,
} from "@streamlit/lib/src/components/shared/Icon"
import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown"
import { notNullOrUndefined } from "@streamlit/lib/src/util/utils"
import { LibContext } from "@streamlit/lib/src/components/core/LibContext"
import { IconSize, isPresetTheme } from "@streamlit/lib/src/theme"

import {
StyledExpandableContainer,
StyledIconContainer,
} from "./styled-components"

export interface ExpanderIconProps {
icon: string
}

import { StyledExpandableContainer } from "./styled-components"
/**
* Renders an icon for the expander.
*
* If the icon is "spinner", it will render a spinner icon.
* If the icon is "check", it will render a check icon.
* If the icon is "error", it will render an error icon.
* Otherwise, it will render nothing.
*
* @param {string} icon - The icon to render.
* @returns {ReactElement}
*/
export const ExpanderIcon = (props: ExpanderIconProps): ReactElement => {
LukasMasuch marked this conversation as resolved.
Show resolved Hide resolved
const { icon } = props
const { activeTheme } = React.useContext(LibContext)

const iconProps = {
size: "lg" as IconSize,
margin: "",
padding: "",
}
if (icon === "spinner") {
const usingCustomTheme = !isPresetTheme(activeTheme)
return (
<StyledSpinnerIcon
usingCustomTheme={usingCustomTheme}
data-testid="stExpanderIconSpinner"
{...iconProps}
/>
)
} else if (icon === "check") {
return (
<StyledIcon
as={Check}
color={"inherit"}
aria-hidden="true"
data-testid="stExpanderIconCheck"
{...iconProps}
/>
)
} else if (icon === "error") {
return (
<StyledIcon
as={ErrorOutline}
color={"inherit"}
aria-hidden="true"
data-testid="stExpanderIconError"
{...iconProps}
/>
)
}

return <></>
}

export interface ExpanderProps {
element: BlockProto.Expandable
Expand All @@ -45,10 +117,12 @@ const Expander: React.FC<ExpanderProps> = ({
children,
}): ReactElement => {
const { label, expanded: initialExpanded } = element

const [expanded, setExpanded] = useState<boolean>(initialExpanded)
const [expanded, setExpanded] = useState<boolean>(initialExpanded || false)
useEffect(() => {
setExpanded(initialExpanded)
// Only apply the expanded state if it was actually set in the proto.
if (notNullOrUndefined(initialExpanded)) {
setExpanded(initialExpanded)
}
// Having `label` in the dependency array here is necessary because
// sometimes two distinct expanders look so similar that even the react
// diffing algorithm decides that they're the same element with updated
Expand All @@ -59,14 +133,22 @@ const Expander: React.FC<ExpanderProps> = ({
// expander's `expanded` state in this edge case.
}, [label, initialExpanded])

const toggle = (): void => setExpanded(!expanded)
const toggle = (): void => {
if (!empty) {
setExpanded(!expanded)
}
}
const { colors, radii, spacing, fontSizes } = useTheme()

return (
<StyledExpandableContainer data-testid="stExpander">
<StyledExpandableContainer
data-testid="stExpander"
empty={empty}
disabled={widgetsDisabled}
>
<Accordion
onChange={toggle}
expanded={expanded ? ["panel"] : []}
expanded={expanded && !empty ? ["panel"] : []}
disabled={widgetsDisabled}
overrides={{
Content: {
Expand Down Expand Up @@ -136,7 +218,6 @@ const Expander: React.FC<ExpanderProps> = ({
}),
props: {
className: "streamlit-expanderHeader",
isStale,
LukasMasuch marked this conversation as resolved.
Show resolved Hide resolved
},
},
ToggleIcon: {
Expand All @@ -145,16 +226,26 @@ const Expander: React.FC<ExpanderProps> = ({
}),
// eslint-disable-next-line react/display-name
component: () => {
if (expanded) {
return <Icon content={ExpandLess} size="lg" />
if (empty) {
// Don't show then expand/collapse icon if there's no content.
return <></>
}
return <Icon content={ExpandMore} size="lg" />
return (
<StyledIcon
as={expanded ? ExpandLess : ExpandMore}
color={"inherit"}
aria-hidden="true"
data-testid="stExpanderToggleIcon"
size="lg"
margin=""
padding=""
/>
)
},
},
Root: {
props: {
className: classNames("streamlit-expander", { empty }),
isStale,
className: classNames("streamlit-expander"),
},
style: {
borderStyle: "solid",
Expand All @@ -173,7 +264,10 @@ const Expander: React.FC<ExpanderProps> = ({
>
<Panel
title={
<StreamlitMarkdown source={label} allowHTML={false} isLabel />
<StyledIconContainer>
{element.icon && <ExpanderIcon icon={element.icon} />}
<StreamlitMarkdown source={label} allowHTML={false} isLabel />
</StyledIconContainer>
}
key="panel"
>
Expand Down