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

feat: plugin sizing elaboration #1398

Merged
merged 9 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions docs/components/Plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ const MyPlugin = (propsFromParent) => {

## Plugin Props (reserved props)

| Name | Type | Required | Description |
| :--------------------: | :------------: | :---------------------------------------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **pluginShortName** | _string_ | _required_ if `pluginSource` is not provided | The shortName of the app/plugin you wish to load (matching the result from api/apps). Used to look up the plugin entry point. If this is not provided, `pluginSource` must be provided. `pluginSource` will take precedence if provided. |
| **pluginSource** | _string_ (url) | _required_ if `pluginShortName` is not provided | The URL of the plugin. If this is not provided, `pluginShortName` must be provided. |
| **onError** | _Function_ | _optional_ | Callback function to be called when an error in the plugin triggers an error boundary. You can use this to pass an error back up to the app and create a custom handling/UX if errors occur in the plugin. In general, it is recommended that you use the plugin's built-in error boundaries |
| **showAlertsInPlugin** | _boolean_ | _optional_ | If `true`, any alerts within the plugin (defined with the `useAlert` hook) will be rendered within the iframe. By default, this is `false`. It is recommended, in general, that you do not override this and allow alerts to be hoisted up to the app level |
| **height** | _string_ | _optional_ | If a height is provided, the iframe will be fixed to the specified height. If no height is provided, the iframe will automatically resize its height based on its contents. The value of `height` will not be passed to the plugin, as it is in an internal implementation detail. If you do need to also pass the height to the plugin, you can pass another variable (e.g. `pluginHeight`). |
| **width** | _string_ | _optional_ | If a width is provided, the iframe will be fixed to the specified width. If no width is provided, the iframe will automatically resize its width based on its contents. The value of `width` will not be passed to the plugin, as it is in an internal implementation detail. If you do need to also pass the width to the plugin, you can pass another variable (e.g. `pluginWidth`). |
| Name | Type | Required | Description |
| :--------------------: | :----------------: | :---------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **pluginShortName** | _string_ | _required_ if `pluginSource` is not provided | The shortName of the app/plugin you wish to load (matching the result from api/apps). Used to look up the plugin entry point. If this is not provided, `pluginSource` must be provided. `pluginSource` will take precedence if provided. |
| **pluginSource** | _string_ (url) | _required_ if `pluginShortName` is not provided | The URL of the plugin. If this is not provided, `pluginShortName` must be provided. |
| **onError** | _Function_ | _optional_ | Callback function to be called when an error in the plugin triggers an error boundary. You can use this to pass an error back up to the app and create a custom handling/UX if errors occur in the plugin. In general, it is recommended that you use the plugin's built-in error boundaries |
| **showAlertsInPlugin** | _boolean_ | _optional_ | If `true`, any alerts within the plugin (defined with the `useAlert` hook) will be rendered within the iframe. By default, this is `false`. It is recommended, in general, that you do not override this and allow alerts to be hoisted up to the app level |
| **height** | _string or number_ | _optional_ | If a height is provided, the iframe will be fixed to the specified height. It can be any valid CSS dimension. By default, if no height is provided, the iframe will automatically resize its height based on its contents, in order to match the behavior of a normal block element. The value of `height` will not be passed to the plugin, as it is in an internal implementation detail. If you do need to also pass the height to the plugin, you can pass another variable (e.g. `pluginHeight`). |
| **width** | _string or number_ | _optional_ | Width for the `iframe` element to use; can be any valid CSS dimension. The default is `100%` to approximate the behavior of a normal block element (but not quite, since `auto` is the default for blocks, but that doesn't work for `iframe`s). If you want the width to resize based on the size of the Plugin's contents, use the `contentWidth` prop instead. The value of `width` will not be passed to the plugin, as it is in an internal implementation detail. If you do need to also pass the width to the plugin, you can pass another variable (e.g. `pluginWidth`). |
| **contentWidth** | _string_ | _optional_ | Set this if you want the width of the iframe to be driven by the contents inside the plugin. The value provided here will be used as the `width` of a `div` wrapping the plugin contents, which will be watched with a resize observer to update the size of the iframe according to the plugin content width. Therefore, **`'max-content'`** is probably the value you want to use for this prop. `'fit-content'` or `'min-content'` may also work, depending on your use case. |
| **className** | _string_ | _optional_ | A `className` value to be used on the `iframe` element to add styles. Sizing styles will take precedence over `width` and `height` props. Flex styles can be used, for example. **NB:** If you want to use this to add a margin, and you're using default width (or have set it `100%` yourself), you should instead wrap the `Plugin` with a div and add the margin on that div to approximate normal behavior of a block element |

## Plugin Props (custom props)

Expand Down
98 changes: 77 additions & 21 deletions services/plugin/src/Plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,50 @@ import postRobot from 'post-robot'
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import PluginError from './PluginError'

type PluginProps = {
/** URL to provide to iframe `src` */
pluginSource?: string
/**
* Short name of the target app/plugin to load -- its plugin launch URL
* will be found from the instance's app list (`/api/apps`)
*/
pluginShortName?: string
/**
* A defined height to used for the iframe. By default, the iframe will
* resize to its content's height
*/
height?: string | number
/**
* A defined width to use on the iframe. By default, `100%` is used to
* approximate the styles of a normal block element
*/
width?: string | number
/**
* Styles can be applied with className. Sizing styles will take precedence
* over `width` and `height` props.
*
* **Note:** If using default width and you want to add margins, you will
* probably want to wrap this `Plugin` in a `div` with the margin styles
* instead to achieve the `width: auto` behavior of a normal block element
*/
className?: string
/**
* Set this if you want the width of the iframe to be driven by the
* contents inside the plugin.
*
* The value provided here will be used as the `width` of a `div` wrapping
* the plugin contents, which will be watched with a resize observer to
* update the size of the iframe according to the plugin content width.
*
* Therefore, **`'max-content'`** is probably the value you want to use.
* `'fit-content'` or `'min-content'` may also work, depending on your use
* case.
*/
clientWidth?: string | number
/** Props that will be sent to the plugin */
propsToPassNonMemoized?: any
}

const appsInfoQuery = {
apps: {
resource: 'apps',
Expand All @@ -29,14 +73,10 @@ export const Plugin = ({
pluginShortName,
height,
width,
className,
clientWidth,
...propsToPassNonMemoized
}: {
pluginSource?: string
pluginShortName?: string
height?: string | number
width?: string | number
propsToPass: any
}): JSX.Element => {
}: PluginProps): JSX.Element => {
const iframeRef = useRef<HTMLIFrameElement>(null)

const { add: alertsAdd } = useContext(AlertsManagerContext)
Expand All @@ -55,7 +95,14 @@ export const Plugin = ({
useState<boolean>(false)

const [inErrorState, setInErrorState] = useState<boolean>(false)
// These are height and width values to be set by callbacks passed to the
// plugin (these default sizes will be quickly overwritten by the plugin).
// In order to behave like a normal block element, by default, the height
// will be set by plugin contents, and this state will be used
const [resizedHeight, setPluginHeight] = useState<number>(150)
// ...and by default, plugin width will be defined by the container
// (width = 100%), so this state won't be used unless the `clientWidth`
// prop is used to have plugin width defined by plugin contents
const [resizedWidth, setPluginWidth] = useState<number>(500)

// since we do not know what props are passed, the dependency array has to be keys of whatever is standard prop
Expand Down Expand Up @@ -87,10 +134,15 @@ export const Plugin = ({
const iframeProps = {
...memoizedPropsToPass,
alertsAdd,
// If a dimension is either specified or container-driven,
// don't send a resize callback to the plugin. The plugin can
// use the presence or absence of these callbacks to determine
// how to handle sizing inside
setPluginHeight: !height ? setPluginHeight : null,
setPluginWidth: !width ? setPluginWidth : null,
setPluginWidth: !width && clientWidth ? setPluginWidth : null,
setInErrorState,
setCommunicationReceived,
clientWidth,
}

// if iframe has not sent initial request, set up a listener
Expand Down Expand Up @@ -138,19 +190,23 @@ export const Plugin = ({
)
}

if (pluginEntryPoint) {
return (
<iframe
ref={iframeRef}
src={pluginSource}
width={width ?? resizedWidth + 'px'}
height={height ?? resizedHeight + 'px'}
style={{
border: 'none',
}}
></iframe>
)
if (!pluginEntryPoint) {
return <></>
}

return <></>
return (
<iframe
ref={iframeRef}
src={pluginSource}
// Styles can be added via className. Sizing styles will take
// precedence over the `width` and `height` props
className={className}
// If clientWidth is set, then we want width to be set by plugin
// (resizedWidth). Thereafter, if a width is specified, use that
// Otherwise, use a specified width, or 100% by default
width={clientWidth ? resizedWidth : width ?? '100%'}
height={height ?? resizedHeight}
style={{ border: 'none' }}
/>
)
}
Loading
Loading