import 'swagger-ui-react/swagger-ui.css'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { Breadcrumb, Button, CardContainer, Dropdown, DropdownDivider, DropdownItem, Header, } from 'insomnia-components'; import React, { Fragment, PureComponent } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { unreachableCase } from 'ts-assert-unreachable'; import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs'; import { AUTOBIND_CFG, DashboardSortOrder, } from '../../common/constants'; import { hotKeyRefs } from '../../common/hotkeys'; import { executeHotKey } from '../../common/hotkeys-listener'; import { isNotNullOrUndefined } from '../../common/misc'; import { descendingNumberSort, sortMethodMap } from '../../common/sorting'; import { strings } from '../../common/strings'; import { ApiSpec } from '../../models/api-spec'; import { isRemoteProject } from '../../models/project'; import { isDesign, Workspace, WorkspaceScopeKeys } from '../../models/workspace'; import { WorkspaceMeta } from '../../models/workspace-meta'; import { MemClient } from '../../sync/git/mem-client'; import { initializeLocalBackendProjectAndMarkForSync } from '../../sync/vcs/initialize-backend-project'; import coreLogo from '../images/insomnia-core-logo.png'; import { cloneGitRepository } from '../redux/modules/git'; import { setDashboardSortOrder } from '../redux/modules/global'; import { ForceToWorkspace } from '../redux/modules/helpers'; import { importClipBoard, importFile, importUri } from '../redux/modules/import'; import { activateWorkspace, createWorkspace } from '../redux/modules/workspace'; import { selectDashboardSortOrder } from '../redux/selectors'; import SettingsButton from './buttons/settings-button'; import AccountDropdown from './dropdowns/account-dropdown'; import { DashboardSortDropdown } from './dropdowns/dashboard-sort-dropdown'; import { ProjectDropdown } from './dropdowns/project-dropdown'; import { RemoteWorkspacesDropdown } from './dropdowns/remote-workspaces-dropdown'; import KeydownBinder from './keydown-binder'; import { showPrompt } from './modals'; import Notice from './notice'; import PageLayout from './page-layout'; import WorkspaceCard, { WorkspaceCardProps } from './workspace-card'; import type { WrapperProps } from './wrapper'; interface Props extends ReturnType, ReturnType { wrapperProps: WrapperProps; } interface State { filter: string; } function orderDashboardCards(orderBy: DashboardSortOrder) { return (cardA: Pick, cardB: Pick) => { switch (orderBy) { case 'modified-desc': return sortMethodMap['modified-desc'](cardA, cardB); case 'name-asc': return sortMethodMap['name-asc'](cardA.workspace, cardB.workspace); case 'name-desc': return sortMethodMap['name-desc'](cardA.workspace, cardB.workspace); case 'created-asc': return sortMethodMap['created-asc'](cardA.workspace, cardB.workspace); case 'created-desc': return sortMethodMap['created-desc'](cardA.workspace, cardB.workspace); default: return unreachableCase(orderBy, `Dashboard ordering "${orderBy}" is invalid`); } }; } const mapWorkspaceToWorkspaceCard = ({ apiSpecs, workspaceMetas, }: { apiSpecs: ApiSpec[]; workspaceMetas: WorkspaceMeta[]; }) => (workspace: Workspace) => { const apiSpec = apiSpecs.find(s => s.parentId === workspace._id); // an apiSpec model will always exist because a migration in the workspace forces it to if (!apiSpec) { return null; } let spec: ParsedApiSpec['contents'] = null; let specFormat: ParsedApiSpec['format'] = null; let specFormatVersion: ParsedApiSpec['formatVersion'] = null; try { const result = parseApiSpec(apiSpec.contents); spec = result.contents; specFormat = result.format; specFormatVersion = result.formatVersion; } catch (err) { // Assume there is no spec // TODO: Check for parse errors if it's an invalid spec } // Get cached branch from WorkspaceMeta const workspaceMeta = workspaceMetas?.find( wm => wm.parentId === workspace._id ); const lastActiveBranch = workspaceMeta?.cachedGitRepositoryBranch; const lastCommitAuthor = workspaceMeta?.cachedGitLastAuthor; // WorkspaceMeta is a good proxy for last modified time const workspaceModified = workspaceMeta?.modified || workspace.modified; const modifiedLocally = isDesign(workspace) ? apiSpec.modified : workspaceModified; // Span spec, workspace and sync related timestamps for card last modified label and sort order const lastModifiedFrom = [ workspace?.modified, workspaceMeta?.modified, apiSpec.modified, workspaceMeta?.cachedGitLastCommitTime, ]; const lastModifiedTimestamp = lastModifiedFrom .filter(isNotNullOrUndefined) .sort(descendingNumberSort)[0]; const hasUnsavedChanges = Boolean( isDesign(workspace) && workspaceMeta?.cachedGitLastCommitTime && apiSpec.modified > workspaceMeta?.cachedGitLastCommitTime ); return { hasUnsavedChanges, lastModifiedTimestamp, modifiedLocally, lastCommitTime: workspaceMeta?.cachedGitLastCommitTime, lastCommitAuthor, lastActiveBranch, spec, specFormat, apiSpec, specFormatVersion, workspace, }; }; @autoBindMethodsForReact(AUTOBIND_CFG) class WrapperHome extends PureComponent { state: State = { filter: '', }; _filterInput: HTMLInputElement | null = null; _setFilterInputRef(n: HTMLInputElement) { this._filterInput = n; } _handleFilterChange(e: React.SyntheticEvent) { this.setState({ filter: e.currentTarget.value, }); } _handleDocumentCreate() { this.props.handleCreateWorkspace({ scope: WorkspaceScopeKeys.design, }); } _handleCollectionCreate() { const { handleCreateWorkspace, wrapperProps: { activeProject, vcs, isLoggedIn }, } = this.props; handleCreateWorkspace({ scope: WorkspaceScopeKeys.collection, onCreate: async workspace => { // Don't mark for sync if not logged in at the time of creation if (isLoggedIn && vcs && isRemoteProject(activeProject)) { await initializeLocalBackendProjectAndMarkForSync({ vcs: vcs.newInstance(), workspace }); } }, }); } _handleImportFile() { this.props.handleImportFile({ forceToWorkspace: ForceToWorkspace.new, }); } _handleImportClipBoard() { this.props.handleImportClipboard({ forceToWorkspace: ForceToWorkspace.new, }); } _handleImportUri() { showPrompt({ title: 'Import document from URL', submitName: 'Fetch and Import', label: 'URL', placeholder: 'https://website.com/insomnia-import.json', onComplete: uri => { this.props.handleImportUri(uri, { forceToWorkspace: ForceToWorkspace.new, }); }, }); } _handleWorkspaceClone() { this.props.handleGitCloneWorkspace({ createFsClient: MemClient.createClient, }); } _handleKeyDown(e) { executeHotKey(e, hotKeyRefs.FILTER_DOCUMENTS, () => { if (this._filterInput) { this._filterInput.focus(); } }); } renderCreateMenu() { const button = ( ); return ( New } onClick={this._handleDocumentCreate} > Design Document } onClick={this._handleCollectionCreate} > Request Collection Import From } onClick={this._handleImportFile} > File } onClick={this._handleImportUri} > URL } onClick={this._handleImportClipBoard} > Clipboard } onClick={this._handleWorkspaceClone} > Git Clone ); } renderDashboardMenu() { const { wrapperProps, handleSetDashboardSortOrder, sortOrder } = this.props; const { vcs } = wrapperProps; return (
{this.renderCreateMenu()}
); } render() { const { sortOrder, wrapperProps, handleActivateWorkspace } = this.props; const { workspaces, isLoading, vcs, activeProject, workspaceMetas, apiSpecs, } = wrapperProps; const { filter } = this.state; // Render each card, removing all the ones that don't match the filter const cards = workspaces .map( mapWorkspaceToWorkspaceCard({ workspaceMetas, apiSpecs, }) ) .filter(isNotNullOrUndefined) .sort(orderDashboardCards(sortOrder)) .map(props => ( handleActivateWorkspace({ workspace: props.workspace })} filter={filter} /> )); const countLabel = cards.length === 1 ? strings.document.singular : strings.document.plural; return ( (
Insomnia , }, ]} /> {isLoading ? ( ) : null} } gridRight={ <> } /> )} renderPageBody={() => (

Dashboard

{this.renderDashboardMenu()}
{cards} {filter && cards.length === 0 && ( No documents found for {filter} )}
{cards.length} {countLabel}
)} /> ); } } const mapStateToProps = state => ({ sortOrder: selectDashboardSortOrder(state), }); const mapDispatchToProps = dispatch => { const bound = bindActionCreators( { createWorkspace, cloneGitRepository, importFile, importClipBoard, importUri, setDashboardSortOrder, activateWorkspace, }, dispatch ); return ({ handleCreateWorkspace: bound.createWorkspace, handleGitCloneWorkspace: bound.cloneGitRepository, handleImportFile: bound.importFile, handleImportUri: bound.importUri, handleImportClipboard: bound.importClipBoard, handleSetDashboardSortOrder: bound.setDashboardSortOrder, handleActivateWorkspace: bound.activateWorkspace, }); }; export default connect(mapStateToProps, mapDispatchToProps)(WrapperHome);