# Tiled Viewer

Tiled Viewer is a Dash component that can be embedded in a Dash application to visually browse Tiled data and select data for use with your own custom callbacks.

[Github](https://github.com/bluesky/tiled-viewer-dash)

[React Source Code](https://github.com/bluesky/tiled-viewer-react)

The main source code is written in React with Typescript, then converted into a Dash component.

## Features

- 🔐 **Multiple Authentication Methods** - API key, bearer token, OIDC, username/password
- 🎨 **Flexible UI Modes** - Normal component, button mode, popup modal
- 🔍 **Advanced Search** - Search by ID, metadata, and spec
- 🔗 **Callbacks** - Item selection data with optional auth tokens

## User Setup
Console
```
pip install tiled-viewer
```
Dash App Example
```python
import tiled_viewer
from dash import Dash, callback, html, Input, Output

app = Dash(__name__)

app.layout = html.Div([
    tiled_viewer.TiledViewer(
        id='input',
        tiledBaseUrl='http://127.0.0.1:8000/api/v1',
    ),
    html.Div(id='output')
])


@callback(Output('output', 'children'), Input('input', 'selectedLinks'))
def display_output(links):
    return f"Selected: {links}"


if __name__ == '__main__':
    app.run(debug=True)
```

## Arguments Reference

### Core Arguments

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `tiledBaseUrl` | `string` | - | Base URL for the Tiled server API. ex)`http://localhost:8000/api/v1` |
| `initialPath` | `string` | - | Initial path to navigate to when component loads, will not allow user to select previous containers and will hide them. ex)`bl531/raw` |


### Authentication Arguments

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `apiKey` | `string` | - | API key for authentication with the Tiled server. Used on all requests when provided. |
| `bearerToken` | `string` | - | Bearer token for authentication as an alternative to API key |
| `includeAuthTokensInSelectCallback` | `boolean` | `false` | Include authentication tokens in the selection callback data. Useful when a backend system is passed a Tiled resource url and will need tokens, but the frontend client performed the authorization through OIDC etc. |
| `oidcRedirectUrl` | `string` | - | URL to redirect to after successful OIDC authentication, requires Tiled server configuration that points to general utility site that does redirects off state param (set via the redirect_on_success field in the config.yml) |

### UI Mode Arguments

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `isPopup` | `boolean` | `false` | Display component as a modal popup with overlay |
| `isButtonMode` | `boolean` | `false` | Display as a button that opens the viewer when clicked |
| `buttonModeText` | `string` | `"Select Data"` | Custom text for the button when in button mode |
| `size` | `'small' \| 'medium' \| 'large'` | - | Size preset for the component container |
| `isFullWidth` | `boolean` | `false` | Make the component take full width of its container |
| `closeOnSelect` | `boolean` | `false` | Automatically close the component after item selection |

### Layout Arguments

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `singleColumnMode` | `boolean` | `false` | Display data in a single column layout instead of multi-column |
| `backgroundClassName` | `string` | - | Additional CSS classes for the background container |
| `contentClassName` | `string` | - | Additional CSS classes for the content container |

### Button Mode Configuration

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `inButtonModeShowApiKeyInput` | `boolean` | - | Show API key input field when in button mode |
| `inButtonModeShowReverseSortInput` | `boolean` | - | Show reverse sort toggle when in button mode |
| `inButtonModeShowSelectedData` | `boolean` | - | Display selected item data when in button mode |

### Navigation & Display Arguments

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `initialPath` | `string` | - | Initial path to navigate to when component loads, will not allow user to select previous containers and will hide them |
| `reverseSort` | `boolean` | `true` | Sort items in reverse order (newest first) |
| `pageLimit` | `number` | `25` | Number of items to display per page |
| `reloadLastItemOnStartup` | `boolean` | `false` | Automatically reload the last viewed item on startup |
| `showPlanName` | `boolean` | - | Display plan names in the item listings if it's a Bluesky Run|
| `showPlanStartTime` | `boolean` | - | Display plan start times in the item listings if it's a Bluesky Run|
| `enableStartupScreen` | `boolean` | `false` | Show the initial startup screen for URL configuration |

## Callbacks
When an item is selected from the interface, the `onSelectCallback` receives a `TiledItemSelectionData` object:

```typescript
interface TiledItemSelectionData {
  // Required item information
  id: string;
  ancestors: string[];
  
  // Item links (from TiledItemLinks)
  self: string;
  full?: string;
  block?: string;
  buffers?: string;
  partition?: string;
  search?: string;
  default?: string;
  
  // Optional authentication tokens (when includeAuthTokensInSelectCallback=true)
  refreshToken?: string | null;
  accessToken?: string | null;
  
  // Optional slice information for 3D+ arrays only
  currentSlice?: number[];
}
```

`onSelectCallback` runs internally within the React component. A Dash Python developer will not have direct acccess to the `onSelectCallback` function because this type of prop cannot be directly converted during the build process.

Instead this dictionary can be used by adding an `id` attribute to the Dash component, then this id is linked to a custom callback. An example is shown below.
```python
import tiled_viewer
from dash import Dash, callback, html, Input, Output

app = Dash(__name__)

app.layout = html.Div([
    tiled_viewer.TiledViewer(
        id='unique_id', #<- give TiledViewer a unique id
        tiledBaseUrl='http://127.0.0.1:8000/api/v1',
    ),
    html.Div(id='output')
])


@callback(Output('output', 'children'), Input('unique_id', 'selectedLinks')) #<- reference the id, second arg is selectedLinks
def display_output(links):
    #links is a dictionary containing info on the Tiled item 
    #that was just selected from the TiledViewer interface.
    #Use it however you like!
    return f"Selected: {links}"


if __name__ == '__main__':
    app.run(debug=True)
```

## UI Variants

### 1. Normal Component Mode

Default rendering as a standard Dash component:

```python
tiled_viewer.TiledViewer(
    tiledBaseUrl="https://localhost:8000/api/v1"
)
```

### 2. Button Mode

Renders as a button that opens a full screen modal overlay when clicked:

```python
tiled_viewer.TiledViewer(
    tiledBaseUrl="https://localhost:8000/api/v1",
    isButtonMode=True,
    buttonModeText="Browse Data",  # text inside the button
    inButtonModeShowSelectedData=True  # shows selected data next to button, can be set to False
)
```

### 3. Popup Modal Mode

Renders as a full-screen modal overlay:

```python
tiled_viewer.TiledViewer(
    tiledBaseUrl="https://localhost:8000/api/v1",
    isPopup=True,
    closeOnSelect=True  # optionally close the modal after data is select, set as False to remain open
)
```

### 4. Single Column Mode

Optimized layout for searching a single column of data, immediately closes when an item is clicked:

```python
tiled_viewer.TiledViewer(
    tiledBaseUrl="https://localhost:8000/api/v1",
    singleColumnMode=True
)
```

## Utility Functions
Some utility functions are provided that are useful for parsing out the base url for the `TiledViewer`.

### split_base_uri_tiled_viewer

Use this when you want a base URI suitable for the TiledViewer component, and you want the container segments that come after /metadata/....
```python
from tiled_viewer import split_base_uri_tiled_viewer

uri = "http://127.0.0.1:8000/api/v1/metadata/data/special_container"

tiled_base_url, containers = split_base_uri_tiled_viewer(uri)

print(tiled_base_url)  # "http://127.0.0.1:8000/api/v1"
print(containers)      # ["data", "special_container"]
```


## Development Tips

- **Python Dash Props**: All component properties can be passed as keyword arguments to `tiled_viewer.TiledViewer()`
- **Debugging**: Check browser console for authentication and connection error messages  
- **Styling**: The component includes its CSS automatically when imported

---
# Getting this to work with a Tiled server


## Bluesky Tiled Server Requirements

This component requires a [Bluesky Tiled server](https://github.com/bluesky/tiled) to be setup and reachable from your application.

### Common Connection Issues

#### 1. CORS Configuration

CORS issues are the most common problem. Configure your Tiled server with proper allowed origins:

Bash
```bash
# Starting from the command line
TILED_ALLOW_ORIGINS='["http://localhost:5174", "https://my-website.com"]' tiled serve demo
```

Tiled Config
```yml
# Using a config file
authentication:
  single_user_api_key: ${TILED_SINGLE_USER_API_KEY}
  allow_anonymous_access: true
uvicorn:
  host: 0.0.0.0
  port: 8000
allow_origins:
  - http://localhost:5173 #<- Manually add all browser clients here
  - http://123.456.789:5174
  - https://my-website.com
trees:
...

```

⚠️ **Important**: You cannot use wildcards (`*`) in CORS allowed origins if your Tiled server requires authentication.

#### 2. Authentication Setup

If you get past CORS but receive authorization errors:

- **API Key**: Add `apiKey` prop to the component
- **Bearer Token**: Use `bearerToken` prop for token-based auth  

```python
# API Key authentication
tiled_viewer.TiledViewer(
    tiledBaseUrl="https://localhost:8000/api/v1",
    apiKey="your-static-api-key"
)

# Bearer token authentication  
tiled_viewer.TiledViewer(
    tiledBaseUrl="https://localhost:8000/api/v1",
    bearerToken="your-bearer-token"
)
```

---

## License & Support

This project is part of the [Bluesky Project](https://github.com/bluesky) ecosystem.

- **Issues**: [GitHub Issues](https://github.com/bluesky/tiled-viewer-react/issues)
- **Discussions**: [GitHub Discussions](https://github.com/bluesky/tiled-viewer-react/discussions)
- **Documentation**: [Tiled Documentation](https://blueskyproject.io/tiled/)





# Developer Instructions

Get started with:
1. Install Dash and its dependencies: https://dash.plotly.com/installation
2. Run `python usage.py`
3. Visit http://localhost:8050 in your web browser
### Install dependencies
1. Install npm packages
    ```
    $ npm install
    ```
2. Create a virtual env and activate.
    ```
    $ virtualenv venv
    $ . venv/bin/activate
    ```

    Conda user?
    ```
    $ conda create -n tiled-dash python=3.12
    $ conda activate tiled-dash
    ```

3. Install python packages required to build components.
    ```
    $ pip install -r requirements.txt
    ```
4. Install the python packages for testing (optional)
    ```
    $ pip install -r tests/requirements.txt
    ```

### Write your component code in `src/lib/components/TiledViewer.react.js`.

- The demo app is in `src/demo` and you will import your example component code into your demo app.
- Test your code in a Python environment:
    1. Build your code
        ```
        $ npm run build
        ```
    2. Run and modify the `usage.py` sample dash app:
        ```
        $ python usage.py
        ```
- Write tests for your component.
    - A sample test is available in `tests/test_usage.py`, it will load `usage.py` and you can then automate interactions with selenium.
    - Run the tests with `$ pytest tests`.
    - The Dash team uses these types of integration tests extensively. Browse the Dash component code on GitHub for more examples of testing (e.g. https://github.com/plotly/dash-core-components)
- Add custom styles to your component by putting your custom CSS files into your distribution folder (`tiled_viewer`).
    - Make sure that they are referenced in `MANIFEST.in` so that they get properly included when you're ready to publish your component.
    - Make sure the stylesheets are added to the `_css_dist` dict in `tiled_viewer/__init__.py` so dash will serve them automatically when the component suite is requested.
- [Review your code](./review_checklist.md)

### Create a production build and publish:
Make sure you're in the tiled_viewer folder, not the root project.

1. Remove old React build
    ```
    npm uninstall @blueskyproject/tiled
    ```
2. Install newest React build
    ```
    npm install @blueskyproject/tiled
    ```
    If there are new props, manually add them in `TiledViewer.react.js` so they can be used by the dash component.

3. Increment the version (updates `package.json`)
    ```
    npm version patch
    ```
4. Create js build files

    Always double check you have a new version listed in `package.json` before building and deploying.
    ```
    npm run build
    ```
3. Verify it works in the sample dash app
    ```
    python usage.py
    ```
4. Remove everything in dist to avoid issues
    ```
    rm -rf dist 
    ```
5. Build
    ```
    python setup.py sdist bdist_wheel
    ```
6. Deploy
    ```
    twine upload dist/* 
    ```