|
| 1 | +import * as path from 'path'; |
| 2 | +import React, { FC, useEffect, useState } from 'react'; |
| 3 | +import { useRouteLoaderData } from 'react-router-dom'; |
| 4 | + |
| 5 | +import { |
| 6 | + NPM_PACKAGE_BASE, |
| 7 | + PLUGIN_HUB_BASE, |
| 8 | +} from '../../../common/constants'; |
| 9 | +import { docsPlugins } from '../../../common/documentation'; |
| 10 | +import { createPlugin } from '../../../plugins/create'; |
| 11 | +import type { Plugin } from '../../../plugins/index'; |
| 12 | +import { getPlugins } from '../../../plugins/index'; |
| 13 | +import { reload } from '../../../templating/index'; |
| 14 | +import { useSettingsPatcher } from '../../hooks/use-request'; |
| 15 | +import { RootLoaderData } from '../../routes/root'; |
| 16 | +import { CopyButton } from '../base/copy-button'; |
| 17 | +import { Link } from '../base/link'; |
| 18 | +import { HelpTooltip } from '../help-tooltip'; |
| 19 | +import { showAlert, showPrompt } from '../modals'; |
| 20 | +import { Button } from '../themed-button'; |
| 21 | +interface State { |
| 22 | + plugins: Plugin[]; |
| 23 | + npmPluginValue: string; |
| 24 | + error: Error | null; |
| 25 | + installPluginErrMsg: string; |
| 26 | + isInstallingFromNpm: boolean; |
| 27 | + isRefreshingPlugins: boolean; |
| 28 | +} |
| 29 | +export const Plugins: FC = () => { |
| 30 | + const [state, setState] = useState<State>({ |
| 31 | + plugins: [], |
| 32 | + npmPluginValue: '', |
| 33 | + error: null, |
| 34 | + installPluginErrMsg: '', |
| 35 | + isInstallingFromNpm: false, |
| 36 | + isRefreshingPlugins: false, |
| 37 | + }); |
| 38 | + const { |
| 39 | + plugins, |
| 40 | + error, |
| 41 | + installPluginErrMsg, |
| 42 | + isInstallingFromNpm, |
| 43 | + isRefreshingPlugins, |
| 44 | + npmPluginValue, |
| 45 | + } = state; |
| 46 | + const { |
| 47 | + settings, |
| 48 | + } = useRouteLoaderData('root') as RootLoaderData; |
| 49 | + |
| 50 | + useEffect(() => { |
| 51 | + refreshPlugins(); |
| 52 | + }, []); |
| 53 | + |
| 54 | + async function refreshPlugins() { |
| 55 | + setState(state => ({ ...state, isRefreshingPlugins: true })); |
| 56 | + // Get and reload plugins |
| 57 | + const plugins = await getPlugins(true); |
| 58 | + reload(); |
| 59 | + |
| 60 | + setState(state => ({ ...state, plugins, isRefreshingPlugins: false })); |
| 61 | + } |
| 62 | + const patchSettings = useSettingsPatcher(); |
| 63 | + |
| 64 | + return ( |
| 65 | + <div> |
| 66 | + <p className="notice info no-margin-top"> |
| 67 | + Plugins is still an experimental feature. See{' '} |
| 68 | + <Link href={docsPlugins}>Documentation</Link> for more info. |
| 69 | + </p> |
| 70 | + {plugins.length === 0 ? ( |
| 71 | + <div className="text-center faint italic pad">No Plugins Added</div> |
| 72 | + ) : ( |
| 73 | + <table className="table--fancy table--striped table--valign-middle margin-top margin-bottom"> |
| 74 | + <thead> |
| 75 | + <tr> |
| 76 | + <th>Enable?</th> |
| 77 | + <th>Name</th> |
| 78 | + <th>Version</th> |
| 79 | + <th>Folder</th> |
| 80 | + </tr> |
| 81 | + </thead> |
| 82 | + <tbody> |
| 83 | + {plugins.map(plugin => { |
| 84 | + const link = path.join(/^insomnia-plugin-/.test(plugin.name) ? PLUGIN_HUB_BASE : NPM_PACKAGE_BASE, plugin.name); |
| 85 | + return !plugin.directory ? null : ( |
| 86 | + <tr key={plugin.name}> |
| 87 | + <td style={{ width: '4rem' }}> |
| 88 | + <input |
| 89 | + type="checkbox" |
| 90 | + checked={!plugin.config.disabled} |
| 91 | + disabled={isRefreshingPlugins} |
| 92 | + onChange={async event => { |
| 93 | + const newConfig = { ...plugin.config, disabled: !event.target.checked }; |
| 94 | + setState(state => ({ ...state, isRefreshingPlugins: true })); |
| 95 | + patchSettings({ pluginConfig: { ...settings.pluginConfig, [plugin.name]: newConfig } }); |
| 96 | + refreshPlugins(); |
| 97 | + }} |
| 98 | + /> |
| 99 | + </td> |
| 100 | + <td> |
| 101 | + {plugin.name} |
| 102 | + {plugin.description && ( |
| 103 | + <HelpTooltip info className="space-left"> |
| 104 | + {plugin.description} |
| 105 | + </HelpTooltip> |
| 106 | + )} |
| 107 | + </td> |
| 108 | + <td> |
| 109 | + {plugin.version} |
| 110 | + <a className="space-left" href={link} title={link}> |
| 111 | + <i className="fa fa-external-link-square" /> |
| 112 | + </a> |
| 113 | + </td> |
| 114 | + <td |
| 115 | + className="no-wrap" |
| 116 | + style={{ |
| 117 | + width: '10rem', |
| 118 | + }} |
| 119 | + > |
| 120 | + <CopyButton |
| 121 | + size="small" |
| 122 | + variant="contained" |
| 123 | + title={plugin.directory} |
| 124 | + content={plugin.directory} |
| 125 | + > |
| 126 | + Copy Path |
| 127 | + </CopyButton>{' '} |
| 128 | + <Button |
| 129 | + size="small" |
| 130 | + variant="contained" |
| 131 | + onClick={() => window.shell.showItemInFolder(plugin.directory)} |
| 132 | + > |
| 133 | + Reveal Folder |
| 134 | + </Button> |
| 135 | + </td> |
| 136 | + </tr> |
| 137 | + ); |
| 138 | + } |
| 139 | + )} |
| 140 | + </tbody> |
| 141 | + </table> |
| 142 | + )} |
| 143 | + |
| 144 | + {error && ( |
| 145 | + <div className="notice error text-left margin-bottom"> |
| 146 | + <button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: null }))}> |
| 147 | + <i className="fa fa-times" /> |
| 148 | + </button> |
| 149 | + <div className="selectable force-pre-wrap"> |
| 150 | + <b>{installPluginErrMsg}</b> |
| 151 | + {'\n\nThere may be an issue with the plugin itself, as a note you can discover and install plugins from the '} |
| 152 | + <a href={PLUGIN_HUB_BASE}>Plugin Hub.</a> |
| 153 | + <details> |
| 154 | + <summary>Additional Information</summary> |
| 155 | + <pre className="pad-top-sm force-wrap selectable"> |
| 156 | + <code>{error.stack || error.message}</code> |
| 157 | + </pre> |
| 158 | + </details> |
| 159 | + </div> |
| 160 | + </div> |
| 161 | + )} |
| 162 | + |
| 163 | + <form |
| 164 | + onSubmit={async event => { |
| 165 | + event.preventDefault(); |
| 166 | + setState(state => ({ ...state, isInstallingFromNpm: true })); |
| 167 | + const newState: Partial<State> = { |
| 168 | + isInstallingFromNpm: false, |
| 169 | + error: null, |
| 170 | + installPluginErrMsg: '', |
| 171 | + }; |
| 172 | + try { |
| 173 | + await window.main.installPlugin(npmPluginValue.trim()); |
| 174 | + await refreshPlugins(); |
| 175 | + newState.npmPluginValue = ''; // Clear input if successful install |
| 176 | + } catch (err) { |
| 177 | + newState.installPluginErrMsg = `Failed to install ${npmPluginValue}`; |
| 178 | + newState.error = err; |
| 179 | + } |
| 180 | + setState(state => ({ ...state, ...newState })); |
| 181 | + }} |
| 182 | + > |
| 183 | + <div className="form-row"> |
| 184 | + <div className="form-control form-control--outlined"> |
| 185 | + <input |
| 186 | + onChange={event => { |
| 187 | + if (event.target instanceof HTMLInputElement) { |
| 188 | + setState(state => ({ ...state, npmPluginValue: event.target.value })); |
| 189 | + } |
| 190 | + }} |
| 191 | + disabled={isInstallingFromNpm} |
| 192 | + type="text" |
| 193 | + placeholder="npm-package-name" |
| 194 | + value={npmPluginValue} |
| 195 | + /> |
| 196 | + </div> |
| 197 | + <div className="form-control width-auto"> |
| 198 | + <Button variant="contained" bg="surprise" disabled={isInstallingFromNpm}> |
| 199 | + {isInstallingFromNpm && <i className="fa fa-refresh fa-spin space-right" />} |
| 200 | + Install Plugin |
| 201 | + </Button> |
| 202 | + </div> |
| 203 | + </div> |
| 204 | + </form> |
| 205 | + <hr /> |
| 206 | + <div className="text-right"> |
| 207 | + <Button |
| 208 | + onClick={() => window.main.openInBrowser(PLUGIN_HUB_BASE)} |
| 209 | + > |
| 210 | + Browse Plugin Hub |
| 211 | + </Button> |
| 212 | + <Button |
| 213 | + style={{ |
| 214 | + marginLeft: '0.3em', |
| 215 | + }} |
| 216 | + onClick={() => showPrompt({ |
| 217 | + title: 'New Plugin', |
| 218 | + defaultValue: 'demo-example', |
| 219 | + placeholder: 'example-name', |
| 220 | + submitName: 'Generate', |
| 221 | + label: 'Plugin Name', |
| 222 | + selectText: true, |
| 223 | + validate: name => |
| 224 | + name.match(/^[a-z][a-z-]*[a-z]$/) ? '' : 'Plugin name must be of format my-plugin-name', |
| 225 | + onComplete: async name => { |
| 226 | + // Remove insomnia-plugin- prefix if they accidentally typed it |
| 227 | + name = name.replace(/^insomnia-plugin-/, ''); |
| 228 | + try { |
| 229 | + await createPlugin( |
| 230 | + `insomnia-plugin-${name}`, |
| 231 | + '0.0.1', |
| 232 | + [ |
| 233 | + '// For help writing plugins, visit the documentation to get started:', |
| 234 | + `// ${docsPlugins}`, |
| 235 | + '', |
| 236 | + '// TODO: Add plugin code here...', |
| 237 | + ].join('\n'), |
| 238 | + ); |
| 239 | + } catch (err) { |
| 240 | + console.error(err); |
| 241 | + showAlert({ |
| 242 | + title: 'Failed to Create Plugin', |
| 243 | + message: err.message, |
| 244 | + }); |
| 245 | + } |
| 246 | + refreshPlugins(); |
| 247 | + }, |
| 248 | + })} |
| 249 | + >Generate New Plugin</Button> |
| 250 | + <Button |
| 251 | + style={{ |
| 252 | + marginLeft: '0.3em', |
| 253 | + }} |
| 254 | + onClick={() => window.shell.showItemInFolder(path.join(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'), 'plugins'))} |
| 255 | + > |
| 256 | + Reveal Plugins Folder |
| 257 | + </Button> |
| 258 | + <Button |
| 259 | + disabled={isRefreshingPlugins} |
| 260 | + style={{ |
| 261 | + marginLeft: '0.3em', |
| 262 | + }} |
| 263 | + onClick={() => refreshPlugins()} |
| 264 | + > |
| 265 | + Reload Plugins |
| 266 | + {isRefreshingPlugins && <i className="fa fa-refresh fa-spin space-left" />} |
| 267 | + </Button> |
| 268 | + </div> |
| 269 | + </div> |
| 270 | + ); |
| 271 | +}; |
0 commit comments