New top bar (#7392)
* wip * wip * New backend switcher * New user bar (user and chat buttons) * Page switcher * New search bar; minor style fixes * Address QA * Refactor HTML `button`s into `Button` component * Add cloud color back to Tailwind * Fix icons shrinking * Fix bug --------- Co-authored-by: Paweł Buchowski <pawel.buchowski@enso.org>
@ -1,5 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="1" width="12" height="3" fill="#767676" />
|
||||
<rect x="2" y="6" width="12" height="3" fill="#767676" />
|
||||
<rect x="2" y="11" width="12" height="3" fill="#767676" />
|
||||
</svg>
|
Before Width: | Height: | Size: 289 B |
7
app/ide-desktop/lib/assets/chat.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.9">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M8 15C12.4183 15 16 11.6421 16 7.5C16 3.35786 12.4183 0 8 0C3.58172 0 0 3.35786 0 7.5C0 9.08162 0.52221 10.5489 1.4138 11.7585L1.12093e-05 16L4.18897 14.0959C5.32198 14.6725 6.6202 15 8 15Z"
|
||||
fill="black" fill-opacity="0.6" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 440 B |
@ -1,4 +1,5 @@
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 16A2.9 2.9 0 1 1 8 10.5 4 4 0 0 1 15.5 11 2 2 0 0 1 17.5 12 1.9 1.9 0 1 1 18.5 16"
|
||||
fill="#3e515fe5" />
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M2.03823 7.31894C2.37639 4.32591 4.91657 2 8 2C10.7676 2 13.0976 3.87386 13.7904 6.42207C15.1006 7.07898 16 8.43446 16 10C16 12.2091 14.2091 14 12 14H8H3.5C1.567 14 0 12.433 0 10.5C0 9.08881 0.835182 7.87268 2.03823 7.31894Z"
|
||||
fill="black" />
|
||||
</svg>
|
Before Width: | Height: | Size: 231 B After Width: | Height: | Size: 413 B |
9
app/ide-desktop/lib/assets/drive.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0 9.5V13.5C0 14.8807 3.58172 16 8 16C12.4183 16 16 14.8807 16 13.5V9.5C16 10.8807 12.4183 12 8 12C3.58172 12 0 10.8807 0 9.5Z"
|
||||
fill="black" fill-opacity="0.6" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0 4V8C0 9.38071 3.58172 10.5 8 10.5C12.4183 10.5 16 9.38071 16 8V4C16 5.38071 12.4183 6.5 8 6.5C3.58172 6.5 0 5.38071 0 4Z"
|
||||
fill="black" fill-opacity="0.6" />
|
||||
<ellipse cx="8" cy="2.5" rx="8" ry="2.5" fill="black" fill-opacity="0.6" />
|
||||
</svg>
|
After Width: | Height: | Size: 643 B |
7
app/ide-desktop/lib/assets/find.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.8">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11 6.5C11 8.98528 8.98528 11 6.5 11C4.01472 11 2 8.98528 2 6.5C2 4.01472 4.01472 2 6.5 2C8.98528 2 11 4.01472 11 6.5ZM9.91218 12.0334C8.9204 12.6463 7.7515 13 6.5 13C2.91015 13 0 10.0899 0 6.5C0 2.91015 2.91015 0 6.5 0C10.0899 0 13 2.91015 13 6.5C13 7.75147 12.6463 8.92033 12.0335 9.91209L15.5601 13.4387C16.1458 14.0244 16.1458 14.9742 15.5601 15.56C14.9743 16.1458 14.0245 16.1458 13.4387 15.56L9.91218 12.0334Z"
|
||||
fill="black" fill-opacity="0.6" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 666 B |
4
app/ide-desktop/lib/assets/home.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 5H0V16H5V10H8V16H13V5Z" fill="black" fill-opacity="0.6" />
|
||||
<path d="M6.5 0L13 5H0L6.5 0Z" fill="black" fill-opacity="0.6" />
|
||||
</svg>
|
After Width: | Height: | Size: 288 B |
@ -1,6 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M11.4142 10L15.6569 14.2426L14.2426 15.6569L10 11.4142L11.4142 10Z" fill="#3e515fe5" />
|
||||
<circle cx="7" cy="7" r="5" stroke="#3e515fe5" stroke-width="2" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 313 B |
5
app/ide-desktop/lib/assets/network.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M0 0H16V4H13.5V6H16V10H9V6H11.5V4H4.5V6H7V10H4.5V12H16V16H0V12H2.5V10H0V6H2.5V4H0V0Z" fill="black"
|
||||
fill-opacity="0.6" />
|
||||
</svg>
|
After Width: | Height: | Size: 292 B |
5
app/ide-desktop/lib/assets/not_cloud.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M1.12012 11.776L12.7387 5.06807C12.7403 5.06838 12.742 5.06869 12.7437 5.06901L16.0071 3.18391L16.0071 5.49044L14.828 6.17119C15.5521 6.89508 16 7.89524 16 9C16 11.2091 14.2091 13 12 13H4H3.5V12.9691C3.38338 12.9545 3.26833 12.9349 3.15505 12.9106L0.00883 14.7271L0 12.4312L1.12462 11.7807C1.12312 11.7791 1.12162 11.7776 1.12012 11.776ZM11.5851 4.00268L0.29851 10.519C0.1061 10.0506 0 9.53771 0 9C0 7.51015 0.81451 6.21055 2.02253 5.52219C2.26308 2.9849 4.39974 1 7 1C9.0514 1 10.8142 2.23534 11.5851 4.00268Z"
|
||||
fill="black" />
|
||||
</svg>
|
After Width: | Height: | Size: 699 B |
5
app/ide-desktop/lib/assets/settings.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M7.23515 0C6.79777 0 6.41114 0.284249 6.28067 0.701725L5.73703 2.4414C5.22538 2.64991 4.74898 2.92701 4.31925 3.26128L2.5392 2.86189C2.11243 2.76613 1.67295 2.95884 1.45425 3.33763L0.689444 4.66232C0.470751 5.0411 0.523601 5.51806 0.819915 5.83978L2.05539 7.18119C2.01886 7.44891 1.99999 7.72225 1.99999 8C1.99999 8.27774 2.01886 8.55107 2.05539 8.81878L0.819912 10.1602C0.523598 10.4819 0.470747 10.9589 0.68944 11.3376L1.45425 12.6623C1.67294 13.0411 2.11243 13.2338 2.5392 13.1381L4.31922 12.7387C4.74897 13.073 5.2254 13.3501 5.73708 13.5586L6.28072 15.2983C6.41118 15.7157 6.79781 16 7.2352 16H8.76481C9.2022 16 9.58883 15.7157 9.71929 15.2983L10.2629 13.5586C10.7746 13.3501 11.251 13.073 11.6808 12.7387L13.4609 13.1381C13.8876 13.2338 14.3271 13.0411 14.5458 12.6623L15.3106 11.3376C15.5293 10.9589 15.4765 10.4819 15.1801 10.1602L13.9446 8.8187C13.9811 8.55101 14 8.27771 14 8C14 7.72228 13.9811 7.44897 13.9446 7.18128L15.1801 5.83981C15.4764 5.51809 15.5293 5.04113 15.3106 4.66234L14.5458 3.33766C14.3271 2.95887 13.8876 2.76616 13.4608 2.86191L11.6808 3.26131C11.251 2.92702 10.7746 2.6499 10.2629 2.44138L9.71925 0.701725C9.58879 0.28425 9.20216 0 8.76477 0H7.23515ZM7.99999 10C9.10456 10 9.99999 9.10457 9.99999 8C9.99999 6.89543 9.10456 6 7.99999 6C6.89542 6 5.99999 6.89543 5.99999 8C5.99999 9.10457 6.89542 10 7.99999 10Z"
|
||||
fill="black" fill-opacity="0.6" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,4 +0,0 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="8" cy="8" rx="8" ry="7.5" fill="white" />
|
||||
<path d="M4.17269e-05 16.5L2 10.5L5.50006 14L4.17269e-05 16.5Z" fill="white" />
|
||||
</svg>
|
Before Width: | Height: | Size: 245 B |
@ -0,0 +1,41 @@
|
||||
/** @file A toolbar for displaying asset information. */
|
||||
import * as React from 'react'
|
||||
|
||||
import DocsIcon from 'enso-assets/docs.svg'
|
||||
import SettingsIcon from 'enso-assets/settings.svg'
|
||||
|
||||
import * as backend from '../backend'
|
||||
import Button from './button'
|
||||
|
||||
/** Props for an {@link AssetInfoBar}. */
|
||||
export interface AssetInfoBarProps {
|
||||
asset: backend.Asset | null
|
||||
}
|
||||
|
||||
/** A toolbar for displaying asset information. */
|
||||
// This parameter will be used in the future.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function AssetInfoBar(_props: AssetInfoBarProps) {
|
||||
return (
|
||||
<div className="flex items-center shrink-0 bg-frame-bg rounded-full gap-3 h-8 px-2">
|
||||
<Button
|
||||
active={false}
|
||||
disabled
|
||||
image={DocsIcon}
|
||||
error="Not implemented yet."
|
||||
onClick={() => {
|
||||
// No backend support yet.
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
active={false}
|
||||
disabled
|
||||
image={SettingsIcon}
|
||||
error="Not implemented yet."
|
||||
onClick={() => {
|
||||
// No backend support yet.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/** @file Switcher for choosing the project management backend. */
|
||||
import * as React from 'react'
|
||||
|
||||
import CloudIcon from 'enso-assets/cloud.svg'
|
||||
import NotCloudIcon from 'enso-assets/not_cloud.svg'
|
||||
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
|
||||
// =======================
|
||||
// === BackendSwitcher ===
|
||||
// =======================
|
||||
|
||||
/** Props for a {@link BackendSwitcher}. */
|
||||
export interface BackendSwitcherProps {
|
||||
setBackendType: (backendType: backendModule.BackendType) => void
|
||||
}
|
||||
|
||||
/** Switcher for choosing the project management backend. */
|
||||
export default function BackendSwitcher(props: BackendSwitcherProps) {
|
||||
const { setBackendType } = props
|
||||
const { backend } = backendProvider.useBackend()
|
||||
return (
|
||||
<div className="flex shrink-0 gap-px">
|
||||
<div
|
||||
className={`rounded-l-full px-2.5 py-1 ${
|
||||
backend.type === backendModule.BackendType.remote
|
||||
? 'bg-frame-selected-bg'
|
||||
: 'bg-frame-bg'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBackendType(backendModule.BackendType.remote)
|
||||
}}
|
||||
disabled={backend.type === backendModule.BackendType.remote}
|
||||
className={`flex items-center gap-2 ${
|
||||
backend.type === backendModule.BackendType.remote
|
||||
? 'text-cloud'
|
||||
: 'text-black opacity-30'
|
||||
}`}
|
||||
>
|
||||
<SvgMask src={CloudIcon} />
|
||||
<span className="leading-5 h-6 py-px">Cloud</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={`rounded-r-full px-2.5 py-1 ${
|
||||
backend.type === backendModule.BackendType.local
|
||||
? 'bg-frame-selected-bg'
|
||||
: 'bg-frame-bg'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBackendType(backendModule.BackendType.local)
|
||||
}}
|
||||
disabled={backend.type === backendModule.BackendType.local}
|
||||
className={`flex items-center gap-2 ${
|
||||
backend.type === backendModule.BackendType.local
|
||||
? 'text-cloud'
|
||||
: 'text-black opacity-30'
|
||||
}`}
|
||||
>
|
||||
<SvgMask src={NotCloudIcon} />
|
||||
<span className="leading-5 h-6 py-px">Local</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -6,20 +6,25 @@ export interface ButtonProps {
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
image: string
|
||||
/** A title that is only shown when `disabled` is true. */
|
||||
error?: string | null
|
||||
onClick: (event: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
/** A styled button. */
|
||||
export default function Button(props: ButtonProps) {
|
||||
const { active = false, disabled = false, image, onClick } = props
|
||||
const { active = false, disabled = false, image, error, onClick } = props
|
||||
|
||||
return (
|
||||
<img
|
||||
className={`${active ? '' : 'opacity-50'} ${
|
||||
disabled ? '' : 'hover:opacity-100 cursor-pointer'
|
||||
<button
|
||||
disabled={disabled}
|
||||
{...(error != null ? { title: error } : {})}
|
||||
className={`cursor-pointer disabled:cursor-default disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-100 ${
|
||||
active ? '' : 'opacity-50'
|
||||
}`}
|
||||
src={image}
|
||||
onClick={onClick}
|
||||
/>
|
||||
>
|
||||
<img src={image} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
@ -693,7 +693,7 @@ export default function Chat(props: ChatProps) {
|
||||
return reactDom.createPortal(
|
||||
<div
|
||||
style={{ right }}
|
||||
className="text-xs-mini text-chat flex flex-col fixed top-0 right-0 h-screen bg-ide-bg border-ide-bg-dark border-l-2 w-83.5 py-1 z-10"
|
||||
className="text-xs text-chat flex flex-col fixed top-0 right-0 h-screen bg-ide-bg border-ide-bg-dark border-l-2 w-83.5 py-1 z-10"
|
||||
>
|
||||
<ChatHeader
|
||||
threads={threads}
|
||||
|
@ -12,28 +12,21 @@ import * as localBackend from '../localBackend'
|
||||
import * as projectManager from '../projectManager'
|
||||
import * as remoteBackendModule from '../remoteBackend'
|
||||
import * as shortcuts from '../shortcuts'
|
||||
import * as tabModule from '../tab'
|
||||
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as loggerProvider from '../../providers/logger'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
|
||||
import * as pageSwitcher from './pageSwitcher'
|
||||
import * as spinner from './spinner'
|
||||
import Chat, * as chat from './chat'
|
||||
import DirectoryView from './driveView'
|
||||
import Ide from './ide'
|
||||
import DriveView from './driveView'
|
||||
import Editor from './editor'
|
||||
import Templates from './templates'
|
||||
import TheModal from './theModal'
|
||||
import TopBar from './topBar'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The `id` attribute of the element into which the IDE will be rendered. */
|
||||
const IDE_ELEMENT_ID = 'root'
|
||||
|
||||
// =================
|
||||
// === Dashboard ===
|
||||
// =================
|
||||
@ -64,7 +57,7 @@ export default function Dashboard(props: DashboardProps) {
|
||||
const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false)
|
||||
const [isHelpChatVisible, setIsHelpChatVisible] = React.useState(false)
|
||||
const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = React.useState(false)
|
||||
const [tab, setTab] = React.useState(tabModule.Tab.dashboard)
|
||||
const [page, setPage] = React.useState(pageSwitcher.Page.drive)
|
||||
const [project, setProject] = React.useState<backendModule.Project | null>(null)
|
||||
const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] =
|
||||
React.useState(initialProjectName)
|
||||
@ -80,37 +73,9 @@ export default function Dashboard(props: DashboardProps) {
|
||||
session.type === authProvider.UserSessionType.offline &&
|
||||
backend.type === backendModule.BackendType.remote
|
||||
|
||||
const switchToIdeTab = React.useCallback(() => {
|
||||
setTab(tabModule.Tab.ide)
|
||||
React.useEffect(() => {
|
||||
unsetModal()
|
||||
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
||||
if (ideElement) {
|
||||
ideElement.style.top = ''
|
||||
ideElement.style.display = 'absolute'
|
||||
}
|
||||
}, [/* should never change */ unsetModal])
|
||||
|
||||
const switchToDashboardTab = React.useCallback(() => {
|
||||
setTab(tabModule.Tab.dashboard)
|
||||
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
||||
if (ideElement) {
|
||||
ideElement.style.top = '-100vh'
|
||||
ideElement.style.display = 'fixed'
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleTab = React.useCallback(() => {
|
||||
if (project != null && tab === tabModule.Tab.dashboard) {
|
||||
switchToIdeTab()
|
||||
} else {
|
||||
switchToDashboardTab()
|
||||
}
|
||||
}, [
|
||||
project,
|
||||
tab,
|
||||
/* should never change */ switchToDashboardTab,
|
||||
/* should never change */ switchToIdeTab,
|
||||
])
|
||||
}, [page, /* should never change */ unsetModal])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
@ -127,11 +92,14 @@ export default function Dashboard(props: DashboardProps) {
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('show-dashboard', switchToDashboardTab)
|
||||
return () => {
|
||||
document.removeEventListener('show-dashboard', switchToDashboardTab)
|
||||
const goToDrive = () => {
|
||||
setPage(pageSwitcher.Page.drive)
|
||||
}
|
||||
}, [switchToDashboardTab])
|
||||
document.addEventListener('show-dashboard', goToDrive)
|
||||
return () => {
|
||||
document.removeEventListener('show-dashboard', goToDrive)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
// The types come from a third-party API and cannot be changed.
|
||||
@ -224,17 +192,17 @@ export default function Dashboard(props: DashboardProps) {
|
||||
[directoryId, /* should never change */ dispatchAssetListEvent]
|
||||
)
|
||||
|
||||
const doOpenIde = React.useCallback(
|
||||
const openEditor = React.useCallback(
|
||||
async (newProject: backendModule.ProjectAsset) => {
|
||||
switchToIdeTab()
|
||||
setPage(pageSwitcher.Page.editor)
|
||||
if (project?.projectId !== newProject.id) {
|
||||
setProject(await backend.getProjectDetails(newProject.id, newProject.title))
|
||||
}
|
||||
},
|
||||
[backend, project?.projectId, switchToIdeTab]
|
||||
[backend, project?.projectId, setPage]
|
||||
)
|
||||
|
||||
const doCloseIde = React.useCallback(() => {
|
||||
const closeEditor = React.useCallback(() => {
|
||||
setProject(null)
|
||||
}, [])
|
||||
|
||||
@ -246,8 +214,8 @@ export default function Dashboard(props: DashboardProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-2 relative select-none text-primary text-xs h-screen py-2 ${
|
||||
tab === tabModule.Tab.dashboard ? '' : 'hidden'
|
||||
className={`flex flex-col gap-2 relative select-none text-primary text-xs h-screen pb-2 ${
|
||||
page === pageSwitcher.Page.drive ? '' : 'hidden'
|
||||
}`}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault()
|
||||
@ -258,10 +226,12 @@ export default function Dashboard(props: DashboardProps) {
|
||||
<TopBar
|
||||
supportsLocalBackend={supportsLocalBackend}
|
||||
projectName={project?.name ?? null}
|
||||
tab={tab}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
asset={null}
|
||||
isEditorDisabled={project == null}
|
||||
isHelpChatOpen={isHelpChatOpen}
|
||||
setIsHelpChatOpen={setIsHelpChatOpen}
|
||||
toggleTab={toggleTab}
|
||||
setBackendType={setBackendType}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
@ -290,8 +260,8 @@ export default function Dashboard(props: DashboardProps) {
|
||||
) : (
|
||||
<>
|
||||
<Templates onTemplateClick={doCreateProject} />
|
||||
<DirectoryView
|
||||
tab={tab}
|
||||
<DriveView
|
||||
page={page}
|
||||
initialProjectName={initialProjectName}
|
||||
nameOfProjectToImmediatelyOpen={nameOfProjectToImmediatelyOpen}
|
||||
setNameOfProjectToImmediatelyOpen={setNameOfProjectToImmediatelyOpen}
|
||||
@ -301,8 +271,8 @@ export default function Dashboard(props: DashboardProps) {
|
||||
dispatchAssetListEvent={dispatchAssetListEvent}
|
||||
query={query}
|
||||
doCreateProject={doCreateProject}
|
||||
doOpenIde={doOpenIde}
|
||||
doCloseIde={doCloseIde}
|
||||
doOpenEditor={openEditor}
|
||||
doCloseEditor={closeEditor}
|
||||
appRunner={appRunner}
|
||||
loadingProjectManagerDidFail={loadingProjectManagerDidFail}
|
||||
isListingRemoteDirectoryWhileOffline={isListingRemoteDirectoryWhileOffline}
|
||||
@ -312,7 +282,11 @@ export default function Dashboard(props: DashboardProps) {
|
||||
</>
|
||||
)}
|
||||
<TheModal />
|
||||
{project && <Ide project={project} appRunner={appRunner} />}
|
||||
<Editor
|
||||
visible={page === pageSwitcher.Page.editor}
|
||||
project={project}
|
||||
appRunner={appRunner}
|
||||
/>
|
||||
{/* `session.accessToken` MUST be present in order for the `Chat` component to work. */}
|
||||
{isHelpChatVisible && session.accessToken != null && (
|
||||
<Chat
|
||||
|
@ -9,6 +9,7 @@ import DataUploadIcon from 'enso-assets/data_upload.svg'
|
||||
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import Button from './button'
|
||||
|
||||
// ================
|
||||
// === DriveBar ===
|
||||
@ -51,12 +52,16 @@ export default function DriveBar(props: DriveBarProps) {
|
||||
<div className="flex items-center bg-frame-bg rounded-full gap-3 h-8 px-3">
|
||||
{backend.type !== backendModule.BackendType.local && (
|
||||
<>
|
||||
<button onClick={doCreateDirectory}>
|
||||
<img src={AddFolderIcon} />
|
||||
</button>
|
||||
<button disabled className="opacity-50">
|
||||
<img src={AddConnectorIcon} />
|
||||
</button>
|
||||
<Button active image={AddFolderIcon} onClick={doCreateDirectory} />
|
||||
<Button
|
||||
active
|
||||
disabled
|
||||
image={AddConnectorIcon}
|
||||
error="Not implemented yet."
|
||||
onClick={() => {
|
||||
// No backend support yet.
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
@ -68,20 +73,24 @@ export default function DriveBar(props: DriveBarProps) {
|
||||
className="hidden"
|
||||
onInput={doUploadFiles}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
active
|
||||
disabled={backend.type === backendModule.BackendType.local}
|
||||
className={
|
||||
backend.type === backendModule.BackendType.local ? 'opacity-50' : ''
|
||||
}
|
||||
image={DataUploadIcon}
|
||||
error="Cannot upload files from the local backend."
|
||||
onClick={() => {
|
||||
uploadFilesRef.current?.click()
|
||||
}}
|
||||
>
|
||||
<img src={DataUploadIcon} />
|
||||
</button>
|
||||
<button disabled className="opacity-50">
|
||||
<img src={DataDownloadIcon} />
|
||||
</button>
|
||||
/>
|
||||
<Button
|
||||
active
|
||||
disabled
|
||||
image={DataDownloadIcon}
|
||||
error="Not implemented yet."
|
||||
onClick={() => {
|
||||
// No backend support yet.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,8 +11,8 @@ import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as loggerProvider from '../../providers/logger'
|
||||
import * as tabModule from '../tab'
|
||||
|
||||
import * as pageSwitcher from './pageSwitcher'
|
||||
import AssetsTable from './assetsTable'
|
||||
import DriveBar from './driveBar'
|
||||
|
||||
@ -38,7 +38,7 @@ function regexEscape(string: string) {
|
||||
|
||||
/** Props for a {@link DirectoryView}. */
|
||||
export interface DirectoryViewProps {
|
||||
tab: tabModule.Tab
|
||||
page: pageSwitcher.Page
|
||||
initialProjectName: string | null
|
||||
nameOfProjectToImmediatelyOpen: string | null
|
||||
setNameOfProjectToImmediatelyOpen: (nameOfProjectToImmediatelyOpen: string | null) => void
|
||||
@ -48,8 +48,8 @@ export interface DirectoryViewProps {
|
||||
dispatchAssetListEvent: (directoryEvent: assetListEventModule.AssetListEvent) => void
|
||||
query: string
|
||||
doCreateProject: (templateId: string | null) => void
|
||||
doOpenIde: (project: backendModule.ProjectAsset) => void
|
||||
doCloseIde: () => void
|
||||
doOpenEditor: (project: backendModule.ProjectAsset) => void
|
||||
doCloseEditor: () => void
|
||||
appRunner: AppRunner | null
|
||||
loadingProjectManagerDidFail: boolean
|
||||
isListingRemoteDirectoryWhileOffline: boolean
|
||||
@ -60,7 +60,7 @@ export interface DirectoryViewProps {
|
||||
/** Contains directory path and directory contents (projects, folders, secrets and files). */
|
||||
export default function DirectoryView(props: DirectoryViewProps) {
|
||||
const {
|
||||
tab,
|
||||
page,
|
||||
initialProjectName,
|
||||
nameOfProjectToImmediatelyOpen,
|
||||
setNameOfProjectToImmediatelyOpen,
|
||||
@ -70,8 +70,8 @@ export default function DirectoryView(props: DirectoryViewProps) {
|
||||
assetListEvents,
|
||||
dispatchAssetListEvent,
|
||||
doCreateProject,
|
||||
doOpenIde,
|
||||
doCloseIde,
|
||||
doOpenEditor,
|
||||
doCloseEditor,
|
||||
appRunner,
|
||||
loadingProjectManagerDidFail,
|
||||
isListingRemoteDirectoryWhileOffline,
|
||||
@ -246,7 +246,7 @@ export default function DirectoryView(props: DirectoryViewProps) {
|
||||
React.useEffect(() => {
|
||||
const onDragEnter = (event: DragEvent) => {
|
||||
if (
|
||||
tab === tabModule.Tab.dashboard &&
|
||||
page === pageSwitcher.Page.drive &&
|
||||
event.dataTransfer?.types.includes('Files') === true
|
||||
) {
|
||||
setIsFileBeingDragged(true)
|
||||
@ -256,7 +256,7 @@ export default function DirectoryView(props: DirectoryViewProps) {
|
||||
return () => {
|
||||
document.body.removeEventListener('dragenter', onDragEnter)
|
||||
}
|
||||
}, [tab])
|
||||
}, [page])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 overflow-hidden gap-2.5 px-3.25">
|
||||
@ -281,8 +281,8 @@ export default function DirectoryView(props: DirectoryViewProps) {
|
||||
dispatchAssetEvent={dispatchAssetEvent}
|
||||
assetListEvents={assetListEvents}
|
||||
dispatchAssetListEvent={dispatchAssetListEvent}
|
||||
doOpenIde={doOpenIde}
|
||||
doCloseIde={doCloseIde}
|
||||
doOpenIde={doOpenEditor}
|
||||
doCloseIde={doCloseEditor}
|
||||
/>
|
||||
{isFileBeingDragged &&
|
||||
directoryId != null &&
|
||||
|
@ -0,0 +1,170 @@
|
||||
/** @file Container that launches the editor. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as auth from '../../authentication/providers/auth'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
|
||||
import GLOBAL_CONFIG from '../../../../../../../../gui/config.yaml' assert { type: 'yaml' }
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The `id` attribute of the element into which the IDE will be rendered. */
|
||||
const IDE_ELEMENT_ID = 'root'
|
||||
const IDE_CDN_URL = 'https://cdn.enso.org/ide'
|
||||
const JS_EXTENSION: Record<backendModule.BackendType, string> = {
|
||||
[backendModule.BackendType.remote]: '.js.gz',
|
||||
[backendModule.BackendType.local]: '.js',
|
||||
} as const
|
||||
|
||||
// =================
|
||||
// === Component ===
|
||||
// =================
|
||||
|
||||
/** Props for an {@link Editor}. */
|
||||
export interface EditorProps {
|
||||
visible: boolean
|
||||
project: backendModule.Project | null
|
||||
appRunner: AppRunner
|
||||
}
|
||||
|
||||
/** The container that launches the IDE. */
|
||||
export default function Editor(props: EditorProps) {
|
||||
const { visible, project, appRunner } = props
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { accessToken } = auth.useNonPartialUserSession()
|
||||
|
||||
React.useEffect(() => {
|
||||
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
||||
if (ideElement) {
|
||||
if (visible) {
|
||||
ideElement.style.top = ''
|
||||
ideElement.style.display = 'absolute'
|
||||
} else {
|
||||
ideElement.style.top = '-100vh'
|
||||
ideElement.style.display = 'fixed'
|
||||
}
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
let hasEffectRun = false
|
||||
|
||||
React.useEffect(() => {
|
||||
// This is a hack to work around the IDE WASM not playing nicely with React Strict Mode.
|
||||
// This is unavoidable as the WASM must fully set up to be able to properly drop its assets,
|
||||
// but React re-executes this side-effect faster tha the WASM can do so.
|
||||
if (hasEffectRun) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
hasEffectRun = true
|
||||
if (project != null) {
|
||||
void (async () => {
|
||||
const ideVersion =
|
||||
project.ideVersion?.value ??
|
||||
(backend.type === backendModule.BackendType.remote
|
||||
? await backend.listVersions({
|
||||
versionType: backendModule.VersionType.ide,
|
||||
default: true,
|
||||
})
|
||||
: null)?.[0].number.value
|
||||
const engineVersion =
|
||||
project.engineVersion?.value ??
|
||||
(backend.type === backendModule.BackendType.remote
|
||||
? await backend.listVersions({
|
||||
versionType: backendModule.VersionType.backend,
|
||||
default: true,
|
||||
})
|
||||
: null)?.[0].number.value
|
||||
const jsonAddress = project.jsonAddress
|
||||
const binaryAddress = project.binaryAddress
|
||||
if (ideVersion == null) {
|
||||
throw new Error('Could not get the IDE version of the project.')
|
||||
} else if (engineVersion == null) {
|
||||
throw new Error('Could not get the engine version of the project.')
|
||||
} else if (jsonAddress == null) {
|
||||
throw new Error("Could not get the address of the project's JSON endpoint.")
|
||||
} else if (binaryAddress == null) {
|
||||
throw new Error("Could not get the address of the project's binary endpoint.")
|
||||
} else {
|
||||
let assetsRoot: string
|
||||
switch (backend.type) {
|
||||
case backendModule.BackendType.remote: {
|
||||
assetsRoot = `${IDE_CDN_URL}/${ideVersion}/`
|
||||
break
|
||||
}
|
||||
case backendModule.BackendType.local: {
|
||||
assetsRoot = ''
|
||||
break
|
||||
}
|
||||
}
|
||||
const runNewProject = async () => {
|
||||
const engineConfig =
|
||||
backend.type === backendModule.BackendType.remote
|
||||
? {
|
||||
rpcUrl: jsonAddress,
|
||||
dataUrl: binaryAddress,
|
||||
}
|
||||
: {
|
||||
projectManagerUrl: GLOBAL_CONFIG.projectManagerEndpoint,
|
||||
}
|
||||
await appRunner.runApp(
|
||||
{
|
||||
loader: {
|
||||
assetsUrl: `${assetsRoot}dynamic-assets`,
|
||||
wasmUrl: `${assetsRoot}pkg-opt.wasm`,
|
||||
jsUrl: `${assetsRoot}pkg${JS_EXTENSION[backend.type]}`,
|
||||
},
|
||||
engine: {
|
||||
...engineConfig,
|
||||
preferredVersion: engineVersion,
|
||||
},
|
||||
startup: {
|
||||
project: project.packageName,
|
||||
},
|
||||
},
|
||||
// Here we actually need explicit undefined.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
accessToken ?? undefined
|
||||
)
|
||||
}
|
||||
if (backend.type === backendModule.BackendType.local) {
|
||||
await runNewProject()
|
||||
return
|
||||
} else {
|
||||
const script = document.createElement('script')
|
||||
script.crossOrigin = 'anonymous'
|
||||
script.src = `${IDE_CDN_URL}/${engineVersion}/index.js.gz`
|
||||
script.onload = async () => {
|
||||
document.body.removeChild(script)
|
||||
const originalUrl = window.location.href
|
||||
// The URL query contains commandline options when running in the desktop,
|
||||
// which will break the entrypoint for opening a fresh IDE instance.
|
||||
history.replaceState(null, '', new URL('.', originalUrl))
|
||||
await runNewProject()
|
||||
// Restore original URL so that initialization works correctly on refresh.
|
||||
history.replaceState(null, '', originalUrl)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
const style = document.createElement('link')
|
||||
style.crossOrigin = 'anonymous'
|
||||
style.rel = 'stylesheet'
|
||||
style.href = `${IDE_CDN_URL}/${engineVersion}/style.css`
|
||||
document.body.appendChild(style)
|
||||
return () => {
|
||||
style.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
// The backend MUST NOT be a dependency, since the IDE should only be recreated when a new
|
||||
// project is opened, and a local project does not exist on the cloud and vice versa.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}
|
||||
}, [project, /* should never change */ appRunner])
|
||||
|
||||
return <></>
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
/** @file Container that launches the IDE. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as auth from '../../authentication/providers/auth'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
|
||||
import GLOBAL_CONFIG from '../../../../../../../../gui/config.yaml' assert { type: 'yaml' }
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
const IDE_CDN_URL = 'https://cdn.enso.org/ide'
|
||||
const JS_EXTENSION: Record<backendModule.BackendType, string> = {
|
||||
[backendModule.BackendType.remote]: '.js.gz',
|
||||
[backendModule.BackendType.local]: '.js',
|
||||
} as const
|
||||
|
||||
// =================
|
||||
// === Component ===
|
||||
// =================
|
||||
|
||||
/** Props for an {@link Ide}. */
|
||||
export interface IdeProps {
|
||||
project: backendModule.Project
|
||||
appRunner: AppRunner
|
||||
}
|
||||
|
||||
/** The container that launches the IDE. */
|
||||
export default function Ide(props: IdeProps) {
|
||||
const { project, appRunner } = props
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { accessToken } = auth.useNonPartialUserSession()
|
||||
|
||||
let hasEffectRun = false
|
||||
|
||||
React.useEffect(() => {
|
||||
// This is a hack to work around the IDE WASM not playing nicely with React Strict Mode.
|
||||
// This is unavoidable as the WASM must fully set up to be able to properly drop its assets,
|
||||
// but React re-executes this side-effect faster tha the WASM can do so.
|
||||
if (hasEffectRun) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
hasEffectRun = true
|
||||
void (async () => {
|
||||
const ideVersion =
|
||||
project.ideVersion?.value ??
|
||||
(backend.type === backendModule.BackendType.remote
|
||||
? await backend.listVersions({
|
||||
versionType: backendModule.VersionType.ide,
|
||||
default: true,
|
||||
})
|
||||
: null)?.[0].number.value
|
||||
const engineVersion =
|
||||
project.engineVersion?.value ??
|
||||
(backend.type === backendModule.BackendType.remote
|
||||
? await backend.listVersions({
|
||||
versionType: backendModule.VersionType.backend,
|
||||
default: true,
|
||||
})
|
||||
: null)?.[0].number.value
|
||||
const jsonAddress = project.jsonAddress
|
||||
const binaryAddress = project.binaryAddress
|
||||
if (ideVersion == null) {
|
||||
throw new Error('Could not get the IDE version of the project.')
|
||||
} else if (engineVersion == null) {
|
||||
throw new Error('Could not get the engine version of the project.')
|
||||
} else if (jsonAddress == null) {
|
||||
throw new Error("Could not get the address of the project's JSON endpoint.")
|
||||
} else if (binaryAddress == null) {
|
||||
throw new Error("Could not get the address of the project's binary endpoint.")
|
||||
} else {
|
||||
let assetsRoot: string
|
||||
switch (backend.type) {
|
||||
case backendModule.BackendType.remote: {
|
||||
assetsRoot = `${IDE_CDN_URL}/${ideVersion}/`
|
||||
break
|
||||
}
|
||||
case backendModule.BackendType.local: {
|
||||
assetsRoot = ''
|
||||
break
|
||||
}
|
||||
}
|
||||
const runNewProject = async () => {
|
||||
const engineConfig =
|
||||
backend.type === backendModule.BackendType.remote
|
||||
? {
|
||||
rpcUrl: jsonAddress,
|
||||
dataUrl: binaryAddress,
|
||||
}
|
||||
: {
|
||||
projectManagerUrl: GLOBAL_CONFIG.projectManagerEndpoint,
|
||||
}
|
||||
await appRunner.runApp(
|
||||
{
|
||||
loader: {
|
||||
assetsUrl: `${assetsRoot}dynamic-assets`,
|
||||
wasmUrl: `${assetsRoot}pkg-opt.wasm`,
|
||||
jsUrl: `${assetsRoot}pkg${JS_EXTENSION[backend.type]}`,
|
||||
},
|
||||
engine: {
|
||||
...engineConfig,
|
||||
preferredVersion: engineVersion,
|
||||
},
|
||||
startup: {
|
||||
project: project.packageName,
|
||||
},
|
||||
},
|
||||
// Here we actually need explicit undefined.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
accessToken ?? undefined
|
||||
)
|
||||
}
|
||||
if (backend.type === backendModule.BackendType.local) {
|
||||
await runNewProject()
|
||||
return
|
||||
} else {
|
||||
const script = document.createElement('script')
|
||||
script.crossOrigin = 'anonymous'
|
||||
script.src = `${IDE_CDN_URL}/${engineVersion}/index.js.gz`
|
||||
script.onload = async () => {
|
||||
document.body.removeChild(script)
|
||||
const originalUrl = window.location.href
|
||||
// The URL query contains commandline options when running in the desktop,
|
||||
// which will break the entrypoint for opening a fresh IDE instance.
|
||||
history.replaceState(null, '', new URL('.', originalUrl))
|
||||
await runNewProject()
|
||||
// Restore original URL so that initialization works correctly on refresh.
|
||||
history.replaceState(null, '', originalUrl)
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
const style = document.createElement('link')
|
||||
style.crossOrigin = 'anonymous'
|
||||
style.rel = 'stylesheet'
|
||||
style.href = `${IDE_CDN_URL}/${engineVersion}/style.css`
|
||||
document.body.appendChild(style)
|
||||
return () => {
|
||||
style.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
// The backend MUST NOT be a dependency, since the IDE should only be recreated when a new
|
||||
// project is opened, and a local project does not exist on the cloud and vice versa.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [project, /* should never change */ appRunner])
|
||||
|
||||
return <></>
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/** @file Switcher to choose the currently visible full-screen page. */
|
||||
import * as React from 'react'
|
||||
|
||||
import DriveIcon from 'enso-assets/drive.svg'
|
||||
import HomeIcon from 'enso-assets/home.svg'
|
||||
import NetworkIcon from 'enso-assets/network.svg'
|
||||
|
||||
import Button from './button'
|
||||
|
||||
// ====================
|
||||
// === PageSwitcher ===
|
||||
// ====================
|
||||
|
||||
/** Main content of the screen. Only one should be visible at a time. */
|
||||
export enum Page {
|
||||
home = 'home',
|
||||
drive = 'drive',
|
||||
editor = 'editor',
|
||||
}
|
||||
|
||||
/** Error text for each page. */
|
||||
const ERRORS: Record<Page, string | null> = {
|
||||
[Page.home]: 'Not implemented yet.',
|
||||
[Page.drive]: null,
|
||||
[Page.editor]: 'No project is currently open.',
|
||||
}
|
||||
|
||||
/** Data describing how to display a button for a pageg. */
|
||||
interface PageUIData {
|
||||
page: Page
|
||||
icon: string
|
||||
}
|
||||
|
||||
const PAGE_DATA: PageUIData[] = [
|
||||
{ page: Page.home, icon: HomeIcon },
|
||||
{ page: Page.drive, icon: DriveIcon },
|
||||
{ page: Page.editor, icon: NetworkIcon },
|
||||
]
|
||||
|
||||
/** Props for a {@link PageSwitcher}. */
|
||||
export interface PageSwitcherProps {
|
||||
page: Page
|
||||
setPage: (page: Page) => void
|
||||
isEditorDisabled: boolean
|
||||
}
|
||||
|
||||
/** Switcher to choose the currently visible full-screen page. */
|
||||
export default function PageSwitcher(props: PageSwitcherProps) {
|
||||
const { page, setPage, isEditorDisabled } = props
|
||||
return (
|
||||
<div className="flex shrink-0 gap-4">
|
||||
{PAGE_DATA.map(pageData => {
|
||||
const isDisabled =
|
||||
pageData.page === Page.home ||
|
||||
(pageData.page === Page.editor && isEditorDisabled)
|
||||
return (
|
||||
<Button
|
||||
key={pageData.page}
|
||||
image={pageData.icon}
|
||||
active={page === pageData.page}
|
||||
disabled={isDisabled}
|
||||
error={ERRORS[pageData.page]}
|
||||
onClick={() => {
|
||||
setPage(pageData.page)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,20 +1,14 @@
|
||||
/** @file The top-bar of dashboard. */
|
||||
import * as React from 'react'
|
||||
|
||||
import BarsIcon from 'enso-assets/bars.svg'
|
||||
import CloudIcon from 'enso-assets/cloud.svg'
|
||||
import ComputerIcon from 'enso-assets/computer.svg'
|
||||
import DefaultUserIcon from 'enso-assets/default_user.svg'
|
||||
import MagnifyingGlassIcon from 'enso-assets/magnifying_glass.svg'
|
||||
import SpeechBubbleIcon from 'enso-assets/speech_bubble.svg'
|
||||
import FindIcon from 'enso-assets/find.svg'
|
||||
|
||||
import * as backendModule from '../backend'
|
||||
import * as tabModule from '../tab'
|
||||
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
|
||||
import UserMenu from './userMenu'
|
||||
import PageSwitcher, * as pageSwitcher from './pageSwitcher'
|
||||
import AssetInfoBar from './assetInfoBar'
|
||||
import BackendSwitcher from './backendSwitcher'
|
||||
import UserBar from './userBar'
|
||||
|
||||
// ==============
|
||||
// === TopBar ===
|
||||
@ -25,8 +19,10 @@ export interface TopBarProps {
|
||||
/** Whether the application may have the local backend running. */
|
||||
supportsLocalBackend: boolean
|
||||
projectName: string | null
|
||||
tab: tabModule.Tab
|
||||
toggleTab: () => void
|
||||
page: pageSwitcher.Page
|
||||
setPage: (page: pageSwitcher.Page) => void
|
||||
asset: backendModule.Asset | null
|
||||
isEditorDisabled: boolean
|
||||
setBackendType: (backendType: backendModule.BackendType) => void
|
||||
isHelpChatOpen: boolean
|
||||
setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
|
||||
@ -39,112 +35,42 @@ export interface TopBarProps {
|
||||
export default function TopBar(props: TopBarProps) {
|
||||
const {
|
||||
supportsLocalBackend,
|
||||
projectName,
|
||||
tab,
|
||||
toggleTab,
|
||||
page,
|
||||
setPage,
|
||||
asset,
|
||||
isEditorDisabled,
|
||||
setBackendType,
|
||||
isHelpChatOpen,
|
||||
setIsHelpChatOpen,
|
||||
query,
|
||||
setQuery,
|
||||
} = props
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { updateModal } = modalProvider.useSetModal()
|
||||
|
||||
return (
|
||||
<div className="flex mx-2 h-8">
|
||||
{supportsLocalBackend && (
|
||||
<div className="bg-gray-100 rounded-full flex flex-row flex-nowrap p-1.5">
|
||||
<button
|
||||
onClick={() => {
|
||||
setBackendType(backendModule.BackendType.local)
|
||||
}}
|
||||
className={`${
|
||||
backend.type === backendModule.BackendType.local
|
||||
? 'bg-white shadow-soft'
|
||||
: 'opacity-50'
|
||||
} rounded-full px-1.5 py-1`}
|
||||
>
|
||||
<img src={ComputerIcon} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setBackendType(backendModule.BackendType.remote)
|
||||
}}
|
||||
className={`${
|
||||
backend.type === backendModule.BackendType.remote
|
||||
? 'bg-white shadow-soft'
|
||||
: 'opacity-50'
|
||||
} rounded-full px-1.5 py-1`}
|
||||
>
|
||||
<img src={CloudIcon} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-center bg-label rounded-full pl-1 pr-2.5 mx-2 ${
|
||||
projectName != null ? 'cursor-pointer' : 'opacity-50'
|
||||
}`}
|
||||
onClick={toggleTab}
|
||||
>
|
||||
<span
|
||||
className={`opacity-50 overflow-hidden transition-width nowrap ${
|
||||
tab === tabModule.Tab.dashboard ? 'm-2 w-16' : 'w-0'
|
||||
}`}
|
||||
>
|
||||
{projectName ?? 'Dashboard'}
|
||||
</span>
|
||||
<div className="bg-white shadow-soft rounded-full px-1.5 py-1">
|
||||
<img src={BarsIcon} />
|
||||
</div>
|
||||
<span
|
||||
className={`opacity-50 overflow-hidden transition-width nowrap ${
|
||||
tab === tabModule.Tab.ide ? 'm-2 w-16' : 'w-0'
|
||||
}`}
|
||||
>
|
||||
{projectName ?? 'No project open'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grow flex items-center bg-label rounded-full px-2">
|
||||
<div>
|
||||
<img src={MagnifyingGlassIcon} />
|
||||
</div>
|
||||
<div className="relative flex ml-4.75 mr-2.25 mt-2.25 h-8 gap-6">
|
||||
<PageSwitcher page={page} setPage={setPage} isEditorDisabled={isEditorDisabled} />
|
||||
{supportsLocalBackend && <BackendSwitcher setBackendType={setBackendType} />}
|
||||
<div className="grow" />
|
||||
<div className="search-bar absolute flex items-center text-primary bg-frame-bg rounded-full -translate-x-1/2 gap-2.5 left-1/2 h-8 w-98.25 px-2">
|
||||
<label htmlFor="search">
|
||||
<img src={FindIcon} className="opacity-80" />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
size={1}
|
||||
placeholder="Click here or start typing to search for projects, data connectors, users, and more ..."
|
||||
id="search"
|
||||
placeholder="Type to search for projects, data connectors, users, and more."
|
||||
value={query}
|
||||
onChange={event => {
|
||||
setQuery(event.target.value)
|
||||
}}
|
||||
className="flex-1 mx-2 bg-transparent"
|
||||
className="grow bg-transparent leading-5 h-6 py-px"
|
||||
/>
|
||||
</div>
|
||||
<div className="grow" />
|
||||
{!isHelpChatOpen && (
|
||||
<div
|
||||
className="flex cursor-pointer items-center bg-help rounded-full px-2.5 text-white mx-2"
|
||||
onClick={() => {
|
||||
setIsHelpChatOpen(true)
|
||||
}}
|
||||
>
|
||||
<span className="whitespace-nowrap">help chat</span>
|
||||
<div className="ml-2">
|
||||
<img src={SpeechBubbleIcon} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* User profile and menu. */}
|
||||
<div className="transform w-8">
|
||||
<div
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
updateModal(oldModal => (oldModal?.type === UserMenu ? null : <UserMenu />))
|
||||
}}
|
||||
className="rounded-full w-8 h-8 bg-cover cursor-pointer"
|
||||
>
|
||||
<img src={DefaultUserIcon} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<AssetInfoBar asset={asset} />
|
||||
<UserBar isHelpChatOpen={isHelpChatOpen} setIsHelpChatOpen={setIsHelpChatOpen} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -0,0 +1,45 @@
|
||||
/** @file A toolbar containing chat and the user menu. */
|
||||
import * as React from 'react'
|
||||
|
||||
import ChatIcon from 'enso-assets/chat.svg'
|
||||
import DefaultUserIcon from 'enso-assets/default_user.svg'
|
||||
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
|
||||
import Button from './button'
|
||||
import UserMenu from './userMenu'
|
||||
|
||||
// ===============
|
||||
// === UserBar ===
|
||||
// ===============
|
||||
|
||||
/** Props for a {@link UserBar}. */
|
||||
export interface UserBarProps {
|
||||
isHelpChatOpen: boolean
|
||||
setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
|
||||
}
|
||||
|
||||
/** A toolbar containing chat and the user menu. */
|
||||
export default function UserBar(props: UserBarProps) {
|
||||
const { isHelpChatOpen, setIsHelpChatOpen } = props
|
||||
const { updateModal } = modalProvider.useSetModal()
|
||||
return (
|
||||
<div className="flex shrink-0 items-center bg-frame-bg rounded-full gap-3 h-8 pl-2 pr-0.75">
|
||||
<Button
|
||||
active={isHelpChatOpen}
|
||||
image={ChatIcon}
|
||||
onClick={() => {
|
||||
setIsHelpChatOpen(!isHelpChatOpen)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
updateModal(oldModal => (oldModal?.type === UserMenu ? null : <UserMenu />))
|
||||
}}
|
||||
>
|
||||
<img src={DefaultUserIcon} height={28} width={28} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -60,7 +60,7 @@ export default function UserMenu() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute right-2 top-11 z-10 flex flex-col rounded-md bg-white py-1 border"
|
||||
className="absolute right-4.75 top-11 z-10 flex flex-col rounded-md bg-white py-1 border"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
|
@ -1,12 +0,0 @@
|
||||
/** @file An enum specifying the main content of the screen. Only one should be visible
|
||||
* at a time. */
|
||||
|
||||
// ===========
|
||||
// === Tab ===
|
||||
// ===========
|
||||
|
||||
/** Main content of the screen. Only one should be visible at a time. */
|
||||
export enum Tab {
|
||||
dashboard = 'dashboard',
|
||||
ide = 'ide',
|
||||
}
|
@ -128,10 +128,18 @@ body {
|
||||
.h-templates-custom {
|
||||
height: 21.5rem;
|
||||
|
||||
@media screen and (min-width: 1771px) {
|
||||
@media screen and (min-width: 110.6875rem) {
|
||||
height: 11rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar.search-bar {
|
||||
@media screen and (max-width: 59rem) {
|
||||
position: unset;
|
||||
transform: unset;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointer-events-none-recursive,
|
||||
|
@ -19,7 +19,7 @@ export const theme = {
|
||||
extend: {
|
||||
colors: {
|
||||
/** The default color of all text. */
|
||||
primary: 'rgba(0, 0, 0, 0.6)',
|
||||
primary: 'rgba(0, 0, 0, 0.60)',
|
||||
chat: '#484848',
|
||||
'ide-bg': '#ebeef1',
|
||||
'ide-bg-dark': '#d0d3d6',
|
||||
@ -27,7 +27,9 @@ export const theme = {
|
||||
// Should be `#3e515f14`, but `bg-opacity` does not work with RGBA.
|
||||
label: '#f0f1f3',
|
||||
help: '#3f68ce',
|
||||
cloud: '#0666be',
|
||||
'frame-bg': 'rgba(255, 255, 255, 0.40)',
|
||||
'frame-selected-bg': 'rgba(255, 255, 255, 0.70)',
|
||||
'tag-text': 'rgba(255, 255, 255, 0.90)',
|
||||
'tag-text-2': 'rgba(0, 0, 0, 0.60)',
|
||||
'permission-owner': 'rgba(236, 2, 2, 0.70)',
|
||||
@ -44,12 +46,15 @@ export const theme = {
|
||||
2: '2',
|
||||
},
|
||||
fontSize: {
|
||||
'xs-mini': '0.71875rem',
|
||||
xs: '0.71875rem',
|
||||
vs: '0.8125rem',
|
||||
},
|
||||
spacing: {
|
||||
'0.75': '0.1875rem',
|
||||
'1.75': '0.4375rem',
|
||||
'2.25': '0.5625rem',
|
||||
'3.25': '0.8125rem',
|
||||
'4.75': '1.1875rem',
|
||||
'5.5': '1.375rem',
|
||||
'9.5': '2.375rem',
|
||||
'18': '4.5rem',
|
||||
@ -59,6 +64,7 @@ export const theme = {
|
||||
'54': '13.5rem',
|
||||
'70': '17.5rem',
|
||||
'83.5': '20.875rem',
|
||||
'98.25': '24.5625rem',
|
||||
'140': '35rem',
|
||||
'10lh': '10lh',
|
||||
},
|
||||
|