From 4e9ca0e240a7b9e304f6d07a3dcc9043caac5b0b Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Wed, 30 Apr 2025 20:15:42 -0400 Subject: [PATCH 1/8] first stab at UI overhaul --- webui/src/lib/components/badges.tsx | 19 +- webui/src/lib/components/controls.jsx | 54 +- .../src/lib/components/repository/changes.jsx | 49 +- .../src/lib/components/repository/commits.jsx | 7 +- .../lib/components/repository/dataTable.jsx | 124 ++ .../src/lib/components/repository/layout.jsx | 4 +- webui/src/lib/components/repository/tree.jsx | 123 +- .../lib/components/repository/treeRows.jsx | 2 +- webui/src/main.tsx | 19 + webui/src/pages/auth/login.tsx | 29 +- webui/src/pages/repositories/index.jsx | 222 ++-- .../pages/repositories/repository/changes.jsx | 45 +- .../repositories/repository/commits/index.jsx | 14 +- .../repository/fileRenderers/data.tsx | 18 +- .../repository/fileRenderers/index.tsx | 5 + .../repository/fileRenderers/simple.tsx | 22 +- webui/src/styles/auth.css | 94 ++ .../styles/components/bootstrap-compat.css | 19 + webui/src/styles/components/buttons.css | 114 ++ webui/src/styles/components/cards.css | 24 + webui/src/styles/components/forms.css | 76 ++ webui/src/styles/components/tables.css | 38 + webui/src/styles/components/ui-components.css | 443 +++++++ webui/src/styles/globals.css | 1086 +++-------------- webui/src/styles/navigation/navigation.css | 201 +++ webui/src/styles/objects/diff.css | 106 ++ webui/src/styles/objects/object-viewer.css | 140 +++ webui/src/styles/objects/objects.css | 378 ++++++ webui/src/styles/objects/tree.css | 86 ++ webui/src/styles/objects/upload.css | 44 + .../src/styles/repositories/repositories.css | 224 ++++ 31 files changed, 2715 insertions(+), 1114 deletions(-) create mode 100644 webui/src/styles/auth.css create mode 100644 webui/src/styles/components/bootstrap-compat.css create mode 100644 webui/src/styles/components/buttons.css create mode 100644 webui/src/styles/components/cards.css create mode 100644 webui/src/styles/components/forms.css create mode 100644 webui/src/styles/components/tables.css create mode 100644 webui/src/styles/components/ui-components.css create mode 100644 webui/src/styles/navigation/navigation.css create mode 100644 webui/src/styles/objects/diff.css create mode 100644 webui/src/styles/objects/object-viewer.css create mode 100644 webui/src/styles/objects/objects.css create mode 100644 webui/src/styles/objects/tree.css create mode 100644 webui/src/styles/objects/upload.css create mode 100644 webui/src/styles/repositories/repositories.css diff --git a/webui/src/lib/components/badges.tsx b/webui/src/lib/components/badges.tsx index b185c71c115..0915a924759 100644 --- a/webui/src/lib/components/badges.tsx +++ b/webui/src/lib/components/badges.tsx @@ -4,12 +4,19 @@ import Badge from 'react-bootstrap/Badge'; import Stack from 'react-bootstrap/Stack'; import { FaLock } from 'react-icons/fa'; -export const ReadOnlyBadge: FC<{ readOnly: boolean, style: CSS.Properties }> = ({ readOnly, style }) => { +export const ReadOnlyBadge: FC<{ readOnly: boolean, style?: CSS.Properties }> = ({ readOnly, style = {} }) => { return readOnly ? ( - - - {`Read-only`} - + + + + Read-only + ) : null; -}; \ No newline at end of file +}; diff --git a/webui/src/lib/components/controls.jsx b/webui/src/lib/components/controls.jsx index dbab4069b7f..e4f4b480761 100644 --- a/webui/src/lib/components/controls.jsx +++ b/webui/src/lib/components/controls.jsx @@ -8,7 +8,7 @@ import Tooltip from "react-bootstrap/Tooltip"; import Overlay from "react-bootstrap/Overlay"; import Table from "react-bootstrap/Table"; import {OverlayTrigger} from "react-bootstrap"; -import {CheckIcon, PasteIcon, SearchIcon, SyncIcon} from "@primer/octicons-react"; +import {CheckIcon, PasteIcon, SearchIcon, SyncIcon, AlertIcon, AlertFillIcon} from "@primer/octicons-react"; import {Link} from "./nav"; import { Box, @@ -65,7 +65,16 @@ DebouncedFormControl.displayName = "DebouncedFormControl"; export const Loading = ({message = "Loading..."}) => { return ( - {message} +
+
+
+ Loading... +
+
+
+ {message} +
+
); }; @@ -82,14 +91,35 @@ export const AlertError = ({error, onDismiss = null, className = null}) => { while (err.error) err = err.error; if (err.message) content = err.message; - const alertClassName = `${className} text-wrap text-break`.trim(); + const alertClassName = `${className} text-wrap text-break shadow-sm`.trim(); if (onDismiss !== null) { - return {content}; + return ( + +
+
+ +
+
{content}
+
+
+ ); } return ( - {content} + +
+
+ +
+
{content}
+
+
); }; @@ -296,14 +326,15 @@ export const PrefixSearchWidget = ({ onFilter, text = "Search by Prefix", defaul if (expanded) { return ( -
- + + - + diff --git a/webui/src/lib/components/repository/commits.jsx b/webui/src/lib/components/repository/commits.jsx index 3f304bb6b9d..9db2c571580 100644 --- a/webui/src/lib/components/repository/commits.jsx +++ b/webui/src/lib/components/repository/commits.jsx @@ -6,13 +6,10 @@ import {MetadataRow, MetadataUIButton} from "../../../pages/repositories/reposit import {Link} from "../nav"; import dayjs from "dayjs"; import Card from "react-bootstrap/Card"; -import React, {useContext} from "react"; -import {AppContext} from "../../hooks/appContext"; - +import React from "react"; const CommitActions = ({ repo, commit }) => { - const {state} = useContext(AppContext); - const buttonVariant = state.settings.darkMode ? "outline-light" : "outline-dark"; + const buttonVariant = "light"; return (
diff --git a/webui/src/lib/components/repository/dataTable.jsx b/webui/src/lib/components/repository/dataTable.jsx index e69de29bb2d..5cfc482fd3d 100644 --- a/webui/src/lib/components/repository/dataTable.jsx +++ b/webui/src/lib/components/repository/dataTable.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import Table from 'react-bootstrap/Table'; +import PropTypes from 'prop-types'; + +/** + * A reusable data table component with modern styling + * + * @param {Object} props Component props + * @param {Array} props.columns Array of column definitions with {id, Header, accessor, Cell, className} properties + * @param {Array} props.data Array of data objects to display in the table + * @param {boolean} props.bordered Whether to show borders (default: true) + * @param {boolean} props.hover Whether to show hover effect (default: true) + * @param {boolean} props.responsive Whether the table should be responsive (default: true) + * @param {string} props.className Additional CSS class names + * @returns {JSX.Element} DataTable component + */ +const DataTable = ({ + columns, + data, + bordered = true, + hover = true, + responsive = true, + className = '', +}) => { + if (!data || data.length === 0) { + return ( +
+

No data available

+
+ ); + } + + return ( +
+ + + + {columns.map((column) => ( + + ))} + + + + {data.map((row, rowIndex) => ( + + {columns.map((column, colIndex) => { + const cellValue = column.accessor ? row[column.accessor] : null; + const cellClass = getCellClassName(cellValue); + + return ( + + ); + })} + + ))} + +
+
+ {column.Header} + {column.description && {column.description}} +
+
+ {column.Cell ? column.Cell({ value: cellValue, row }) : formatCellValue(cellValue)} +
+
+ ); +}; + +/** + * Determines the appropriate CSS class based on the data type + * + * @param {any} value The cell value + * @returns {string} CSS class name + */ +const getCellClassName = (value) => { + if (typeof value === 'string') return 'string-cell'; + if (typeof value === 'number') return 'number-cell'; + if (value instanceof Date) return 'date-cell'; + return ''; +}; + +/** + * Formats cell values based on their data type + * + * @param {any} value The cell value + * @returns {string|number|JSX.Element} Formatted value + */ +const formatCellValue = (value) => { + if (value === null || value === undefined) return ''; + + if (typeof value === 'number') { + return value.toLocaleString('en-US'); + } + + if (value instanceof Date) { + return value.toLocaleString(); + } + + return value.toString(); +}; + +DataTable.propTypes = { + columns: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + Header: PropTypes.string.isRequired, + accessor: PropTypes.string, + Cell: PropTypes.func, + className: PropTypes.string, + cellClassName: PropTypes.string, + description: PropTypes.string, + }) + ).isRequired, + data: PropTypes.array.isRequired, + bordered: PropTypes.bool, + hover: PropTypes.bool, + responsive: PropTypes.bool, + className: PropTypes.string, +}; + +export default DataTable; diff --git a/webui/src/lib/components/repository/layout.jsx b/webui/src/lib/components/repository/layout.jsx index e154ce33d41..b9a06444d1e 100644 --- a/webui/src/lib/components/repository/layout.jsx +++ b/webui/src/lib/components/repository/layout.jsx @@ -50,7 +50,9 @@ export const RepositoryPageLayout = ({ fluid = "sm" }) => {
- +
+ +
diff --git a/webui/src/lib/components/repository/tree.jsx b/webui/src/lib/components/repository/tree.jsx index 99f1d664d5a..4e969d8ad45 100644 --- a/webui/src/lib/components/repository/tree.jsx +++ b/webui/src/lib/components/repository/tree.jsx @@ -3,7 +3,6 @@ import React, { useCallback, useState } from "react"; import dayjs from "dayjs"; import { PasteIcon, - DotIcon, DownloadIcon, FileDirectoryIcon, FileIcon, @@ -731,63 +730,75 @@ export const URINavigator = ({ const GetStarted = ({ config, onUpload, onImport }) => { const importDisabled = !config.config.import_support; + return ( - -

To get started with this repository:

- - - - - -  data from {config.config.blockstore_type}. Or, see the  - - docs - -  for other ways to import data to your repository. - - - - - - - -  an object. + +
+ Empty repository +
+ +

Your repository is ready!

+
Let's add some data to get started
+ + + + + +
+ +
+ Import Data + + Import existing data from {config.config.blockstore_type} + + +
+ + + Learn more about importing + + +
-
- - - - - Use  - - DistCp - -  or  - - Rclone - -  to copy data into your repository. + + + + +
+ +
+ Upload Objects + + Upload individual files directly to your repository + + +
+ + Quick and easy for small files + +
diff --git a/webui/src/lib/components/repository/treeRows.jsx b/webui/src/lib/components/repository/treeRows.jsx index d0045bc88fa..f174baa3e08 100644 --- a/webui/src/lib/components/repository/treeRows.jsx +++ b/webui/src/lib/components/repository/treeRows.jsx @@ -97,7 +97,7 @@ export const PrefixTreeEntryRow = ({entry, relativeTo = "", dirExpanded, depth = ); }; const PrefixExpansionSection = ({dirExpanded, onClick}) => { - return ( + return ( {dirExpanded ? : } ) } diff --git a/webui/src/main.tsx b/webui/src/main.tsx index ea0c3b69869..86b6a8491dd 100644 --- a/webui/src/main.tsx +++ b/webui/src/main.tsx @@ -4,6 +4,25 @@ import { createRoot } from 'react-dom/client'; import 'bootstrap/dist/css/bootstrap.css'; import './styles/globals.css'; +// Areas +import './styles/navigation/navigation.css'; +import './styles/repositories/repositories.css'; +import './styles/objects/objects.css'; +import './styles/objects/object-viewer.css'; +import './styles/objects/upload.css'; +import './styles/objects/tree.css'; +import './styles/objects/diff.css'; +import './styles/auth.css'; +// Components +import './styles/components/buttons.css'; +import './styles/components/cards.css'; +import './styles/components/tables.css'; +import './styles/components/forms.css'; +import './styles/components/ui-components.css'; +import './styles/components/bootstrap-compat.css'; +import './styles/quickstart.css'; +import './styles/ghsyntax.css'; + // app and plugins system import LakeFSApp from "./extendable/lakefsApp"; import { PluginManager } from "./extendable/plugins/pluginManager"; diff --git a/webui/src/pages/auth/login.tsx b/webui/src/pages/auth/login.tsx index b19e70c356b..4b33b97a150 100644 --- a/webui/src/pages/auth/login.tsx +++ b/webui/src/pages/auth/login.tsx @@ -31,9 +31,11 @@ const LoginForm = ({loginConfig}: {loginConfig: LoginConfig}) => { return ( - - Login - + + +

Login

+
+ { e.preventDefault() try { @@ -50,16 +52,31 @@ const LoginForm = ({loginConfig}: {loginConfig: LoginConfig}) => { } }}> - + - + {(!!loginError) && } - +
{ loginConfig.fallback_login_url ? diff --git a/webui/src/pages/repositories/index.jsx b/webui/src/pages/repositories/index.jsx index 3ec3c522a59..2f832a648d8 100644 --- a/webui/src/pages/repositories/index.jsx +++ b/webui/src/pages/repositories/index.jsx @@ -8,7 +8,6 @@ import InputGroup from "react-bootstrap/InputGroup"; import ButtonToolbar from "react-bootstrap/ButtonToolbar"; import Modal from "react-bootstrap/Modal"; import Spinner from "react-bootstrap/Spinner"; -import Stack from "react-bootstrap/Stack"; import {RepoIcon, SearchIcon} from "@primer/octicons-react"; import dayjs from "dayjs"; @@ -35,8 +34,8 @@ const LOCAL_BLOCKSTORE_SAMPLE_REPO_DEFAULT_BRANCH = "main"; const CreateRepositoryButton = ({variant = "success", enabled = false, onClick}) => { return ( - ); } @@ -81,14 +80,31 @@ const CreateRepositoryModal = ({show, error, onSubmit, onCancel, inProgress}) => onSubmit, })} - - - + @@ -100,33 +116,43 @@ const GetStarted = ({allowSampleRepoCreation, onCreateSampleRepo, onCreateEmptyR

Welcome to lakeFS!

- -

{`To get started, create your first sample repository.`}
- {`This includes sample data, quickstart instructions, and more!`}
- {`Let's dive in 🤿`}

+ +

+ Create your first sample repository to get started with lakeFS. + This includes sample data, quickstart instructions, and everything + you need to explore lakeFS capabilities. +

+ + {allowSampleRepoCreation && ( +
+ Create Sample Repository} + creatingRepo={creatingRepo} + variant={"success"} + enabled={true} + onClick={onCreateSampleRepo} + /> +
+ )} + + {createRepoError && ( + + {createRepoError.message} + + )} + +
+ Already working with lakeFS? + +
- {allowSampleRepoCreation && - - - Create Sample Repository - } creatingRepo={creatingRepo} variant={"success"} enabled={true} onClick={onCreateSampleRepo}/> - - - } - {createRepoError && - - - {createRepoError.message} - - - } - -
- Already working with lakeFS and just need an empty repository? - -
getting-started
@@ -157,30 +183,63 @@ const RepositoryList = ({ onPaginate, search, after, refresh, allowSampleRepoCre
{results.map(repo => ( - - - - -
- - {repo.id} - -
- -
-

- - created at {dayjs.unix(repo.creation_date).toISOString()} ({dayjs.unix(repo.creation_date).fromNow()})
- default branch: {repo.default_branch},{' '} + + + + {/* Repository Header with Icon, Name, Badge and Creation Date */} +

+
+
+ +
+
+ +
+ {repo.id} +
+ +
+
+ + Created {dayjs.unix(repo.creation_date).fromNow()} + +
+
+
+ +
+
+ + {/* Repository Details in Horizontal Layout */} +
+
+
+
Branch
+
+ {repo.default_branch} +
+
+ {repo.storage_id && repo.storage_id.length && - <>storage: {repo.storage_id},{' '} +
+
Storage
+
+ {repo.storage_id} +
+
} - storage namespace: {repo.storage_namespace} -
-

+ +
+
Namespace
+
+ {repo.storage_namespace} +
+
+
+
@@ -258,28 +317,31 @@ const RepositoriesPage = () => { return ( - {showActionsBar && -
{ e.preventDefault(); }}> - - - - - - - setSearch(event.target.value)} - /> - - - -
- - - -
} + {showActionsBar && + +
{ e.preventDefault(); }}> + + + + + + + setSearch(event.target.value)} + /> + + + +
+ + + +
+ } { + return ( +
+ + +

No Changes Yet

+

+ No uncommitted changes on {reference.id}! +
Upload or modify files to see them appear here. +

+ + Upload Files + +
+
+
+ ); +}; const CommitButton = ({repo, onCommit, enabled = false}) => { @@ -226,7 +262,10 @@ const ChangesBrowser = ({repo, reference, prefix, onSelectRef, }) => { repo={repo} reference={reference} internalReferesh={internalRefresh} prefix={prefix} getMore={getMoreUncommittedChanges} loading={loading} nextPage={nextPage} setAfterUpdated={setAfterUpdated} - onNavigate={onNavigate} onRevert={onReset} changesTreeMessage={changesTreeMessage}/> + onNavigate={onNavigate} onRevert={onReset} changesTreeMessage={changesTreeMessage} + noChangesText="No changes - you can modify this branch by uploading data using the UI or any of the supported SDKs" + emptyStateComponent={} + /> ) } @@ -261,4 +300,4 @@ const RepositoryChangesPage = () => { return ; } -export default RepositoryChangesPage; \ No newline at end of file +export default RepositoryChangesPage; diff --git a/webui/src/pages/repositories/repository/commits/index.jsx b/webui/src/pages/repositories/repository/commits/index.jsx index 5a42b0b1ea9..63438cdef6b 100644 --- a/webui/src/pages/repositories/repository/commits/index.jsx +++ b/webui/src/pages/repositories/repository/commits/index.jsx @@ -1,7 +1,7 @@ -import React, {useContext, useEffect, useState} from "react"; +import React, {useEffect, useState} from "react"; import { useOutletContext } from "react-router-dom"; import dayjs from "dayjs"; -import {BrowserIcon, LinkIcon, PackageIcon, PlayIcon} from "@primer/octicons-react"; +import {BrowserIcon, LinkIcon, PlayIcon} from "@primer/octicons-react"; import {commits} from "../../../../lib/api"; import ButtonGroup from "react-bootstrap/ButtonGroup"; @@ -24,17 +24,15 @@ import RefDropdown from "../../../../lib/components/repository/refDropdown"; import {Link} from "../../../../lib/components/nav"; import {useRouter} from "../../../../lib/hooks/router"; import {RepoError} from "../error"; -import {AppContext} from "../../../../lib/hooks/appContext"; const CommitWidget = ({ repo, commit }) => { - const {state} = useContext(AppContext); - const buttonVariant = state.settings.darkMode ? "outline-light" : "outline-dark"; + const buttonVariant = "light"; return (
-
+
{

- + { tooltip="View Commit Action runs"> - }/> - }/> = ({repoId, refId, path, file {`Showing only the first ${res.numRows.toLocaleString()} rows (out of ${data.numRows.toLocaleString()})`} }
- +
{fields.map((field, i) => )} @@ -117,7 +118,6 @@ export const DuckDBRenderer: FC = ({repoId, refId, path, file return ( ) - })} ))} @@ -174,18 +174,16 @@ const DataRow: FC<{ value: any }> = ({ value }) => { } if (dataType === 'string') { - return + return } if (dataType === 'date') { - return + return } if (dataType === 'number') { - return + return } return ; } - - diff --git a/webui/src/pages/repositories/repository/fileRenderers/index.tsx b/webui/src/pages/repositories/repository/fileRenderers/index.tsx index d251f89a7ec..d0f9b6fc262 100644 --- a/webui/src/pages/repositories/repository/fileRenderers/index.tsx +++ b/webui/src/pages/repositories/repository/fileRenderers/index.tsx @@ -57,6 +57,10 @@ export const Renderers: {[fileType in FileType] : FC } = { } export const guessLanguage = (extension: string | null, contentType: string | null) => { + switch (extension) { + case 'py': + extension = 'python' + } if (extension && SyntaxHighlighter.supportedLanguages.includes(extension)) { return extension; } @@ -147,6 +151,7 @@ export function guessType(contentType: string | null, fileExtension: string | nu case 'txt': case 'text': case 'yaml': + case 'py': case 'yml': case 'json': case 'jsonl': diff --git a/webui/src/pages/repositories/repository/fileRenderers/simple.tsx b/webui/src/pages/repositories/repository/fileRenderers/simple.tsx index ee92b0fdd74..6e9f3727591 100644 --- a/webui/src/pages/repositories/repository/fileRenderers/simple.tsx +++ b/webui/src/pages/repositories/repository/fileRenderers/simple.tsx @@ -5,7 +5,6 @@ import { useAPI } from "../../../../lib/hooks/api"; import { objects, qs } from "../../../../lib/api"; import { AlertError, Loading } from "../../../../lib/components/controls"; import SyntaxHighlighter from "react-syntax-highlighter"; -import { githubGist as syntaxHighlightStyle } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { IpynbRenderer as NbRenderer } from "react-ipynb-renderer"; import { guessLanguage } from "./index"; import { @@ -17,9 +16,9 @@ import { import "react-ipynb-renderer/dist/styles/default.css"; import { useMarkdownProcessor } from "./useMarkdownProcessor"; import { AppContext } from "../../../../lib/hooks/appContext"; -import { dark } from "react-syntax-highlighter/dist/esm/styles/prism"; import {GeoJSONPreview} from "../../../../lib/components/repository/GeoJSONPreview"; + export const ObjectTooLarge: FC = ({ path, sizeBytes }) => { return ( @@ -89,10 +88,27 @@ export const TextRenderer: FC = ({ return ( {text} diff --git a/webui/src/styles/auth.css b/webui/src/styles/auth.css new file mode 100644 index 00000000000..6b8b904fbef --- /dev/null +++ b/webui/src/styles/auth.css @@ -0,0 +1,94 @@ +/* Admin navigation pills */ +.auth-page .nav-tabs .nav-link { + color: var(--text-light); +} + +.auth-page .nav-tabs .nav-link:hover { + color: var(--success); + background-color: rgba(139, 92, 246, 0.05); +} + +.auth-page .nav-tabs .nav-link.active { + color: var(--success); + background-color: rgba(139, 92, 246, 0.1); + transform: translateY(-2px); + position: relative; + scale: 1.05; +} + +.auth-page .nav-tabs .nav-link.active::before { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 2px; + background-color: var(--success); + opacity: 0.7; + transition: opacity var(--transition); +} + +.auth-page .nav-tabs .nav-link.active::after { + background-color: var(--success); +} + +/* Override any blue styling in admin navigation */ +.auth-page .nav-tabs, +.auth-page .nav-item, +.auth-page .nav-link { + border-color: var(--success) !important; +} + +.auth-page .nav-tabs .nav-link.active, +.auth-page .nav-item.active, +.auth-page .nav-link.active { + border-color: var(--success) !important; +} + +.auth-page a:not(.btn) { + color: var(--success); +} + +.auth-page a:not(.btn):hover { + color: var(--success-dark); +} + +/* Force override for nav pills in auth pages */ +.auth-page .nav-pills { + --bs-nav-pills-link-active-bg: var(--success) !important; + --bs-nav-pills-link-active-color: white !important; + --bs-nav-link-color: var(--success) !important; + --bs-nav-link-hover-color: var(--success-dark) !important; +} + +.auth-page .nav-pills .nav-link.active { + background-color: var(--success) !important; +} + +/* Ensure nav pills use our custom colors with higher specificity */ +.auth-page .nav-pills .nav-link.active, +.auth-page .nav-pills .show > .nav-link, +.auth-page .nav.nav-pills .nav-link.active, +.auth-page .nav.nav-pills .show > .nav-link, +body .auth-page .nav-pills .nav-link.active, +body .auth-page .nav-pills .show > .nav-link { + background-color: var(--success) !important; + color: white !important; +} + +.auth-page .nav-pills .nav-link, +.auth-page .nav.nav-pills .nav-link, +body .auth-page .nav-pills .nav-link { + color: var(--success) !important; +} + +.auth-page .nav-pills .nav-link:hover, +.auth-page .nav.nav-pills .nav-link:hover, +body .auth-page .nav-pills .nav-link:hover { + color: var(--success-dark) !important; +} + +/* Direct style override for nav-pills */ +.auth-page .nav-pills .nav-link.active { + background-color: var(--success) !important; +} \ No newline at end of file diff --git a/webui/src/styles/components/bootstrap-compat.css b/webui/src/styles/components/bootstrap-compat.css new file mode 100644 index 00000000000..2afd5a514d4 --- /dev/null +++ b/webui/src/styles/components/bootstrap-compat.css @@ -0,0 +1,19 @@ +/* Bootstrap compatibility classes */ +.bs-success { color: var(--success); } +.bs-danger { color: var(--danger); } +.bs-warning { color: var(--warning); } +.bs-secondary { color: var(--secondary); } +.bs-blue { color: var(--primary); } +.bs-cyan { color: var(--info); } +.bs-pink { color: var(--color-policy-text); } +.bs-teal { color: var(--success-dark); } +.bs-white { color: white; } +.bs-body-bg { background-color: var(--background); } +.bs-tertiary-bg { background-color: var(--background-alt); } +.bs-light-border-subtle { border-color: var(--border-light); } +.bs-border-color { border-color: var(--border); } +.bs-dark-border-subtle { border-color: var(--border-dark); } +.bs-light-text-emphasis { color: var(--text-light); } +.bs-dark-text-emphasis { color: var(--text-dark); } +.bs-body-color { color: var(--text); } +.bs-emphasis-color { color: var(--text-dark); } \ No newline at end of file diff --git a/webui/src/styles/components/buttons.css b/webui/src/styles/components/buttons.css new file mode 100644 index 00000000000..1376c81a36e --- /dev/null +++ b/webui/src/styles/components/buttons.css @@ -0,0 +1,114 @@ +/* Buttons */ +.btn { + font-weight: 500; + border-radius: var(--radius); + padding: 0.5rem 1rem; + transition: all var(--transition-fast); + border: none; +} + +.btn-primary { + background-color: var(--primary); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-dark); +} + +.btn-success { + background-color: var(--success); + color: white; +} + +.btn-success:hover { + background-color: var(--success-dark); +} + +.btn-danger { + background-color: var(--danger); + color: white; +} + +.btn-danger:hover { + background-color: var(--danger-dark); +} + +.btn-secondary { + background-color: #4b5563; /* Gray, closer to background color */ + color: white; +} + +.btn-secondary:hover { + background-color: #374151; /* Darker gray for hover */ +} + +.btn-outline-secondary { + color: #4b5563; + border: 1px solid #4b5563; + background-color: transparent; +} + +.btn-outline-secondary:hover { + background-color: #888888; + color: #000; +} + +.btn-link { + color: var(--primary); + background: none; + padding: 0; + font-weight: 500; +} + +.btn-link:hover { + color: var(--primary-dark); + text-decoration: none; +} + +/* Fix for btn-light in dark mode */ +[data-bs-theme="dark"] .btn-light { + background-color: #374151; + color: #f9fafb; + border-color: #4b5563; +} + +[data-bs-theme="dark"] .btn-light:hover { + background-color: #4b5563; + color: #f9fafb; + border-color: #6b7280; +} + +[data-bs-theme="dark"] .btn-light:focus, +[data-bs-theme="dark"] .btn-light:active { + background-color: #4b5563; + color: #f9fafb; + border-color: #6b7280; + box-shadow: 0 0 0 0.25rem rgba(75, 85, 99, 0.5); +} + +/* Fix for btn-light dropdown toggle in dark mode */ +[data-bs-theme="dark"] .btn-light.dropdown-toggle { + background-color: #374151; + color: #f9fafb; + border-color: #4b5563; +} + +[data-bs-theme="dark"] .btn-light.dropdown-toggle:hover, +[data-bs-theme="dark"] .btn-light.dropdown-toggle:focus, +[data-bs-theme="dark"] .btn-light.dropdown-toggle:active { + background-color: #4b5563; + color: #f9fafb; + border-color: #6b7280; +} + +/* Dark mode fix for btn-outline-secondary */ +[data-bs-theme="dark"] .btn-outline-secondary { + color: #f9fafb; + border-color: #6b7280; +} + +[data-bs-theme="dark"] .btn-outline-secondary:hover { + background-color: #4b5563; + color: #f9fafb; +} \ No newline at end of file diff --git a/webui/src/styles/components/cards.css b/webui/src/styles/components/cards.css new file mode 100644 index 00000000000..3d40c5ac07b --- /dev/null +++ b/webui/src/styles/components/cards.css @@ -0,0 +1,24 @@ +/* Cards */ +.card { + background-color: var(--surface); + border-radius: var(--radius-md); + border: 1px solid var(--border); + box-shadow: var(--shadow-sm); + transition: box-shadow var(--transition), transform var(--transition); + overflow: hidden; +} + +.card:hover { + box-shadow: var(--shadow-md); +} + +.card-header { + background-color: var(--surface); + border-bottom: 1px solid var(--border); + padding: var(--spacing-md) var(--spacing-lg); + font-weight: 600; +} + +.card-body { + padding: var(--spacing-lg); +} \ No newline at end of file diff --git a/webui/src/styles/components/forms.css b/webui/src/styles/components/forms.css new file mode 100644 index 00000000000..d157a234a1d --- /dev/null +++ b/webui/src/styles/components/forms.css @@ -0,0 +1,76 @@ +/* Forms */ +.form-control { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--text); + background-color: var(--surface); + background-clip: padding-box; + border: 1px solid var(--border); + border-radius: var(--radius); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.form-control:focus { + color: var(--text); + background-color: var(--surface); + border-color: var(--primary-light); + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.25); +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--text); + text-align: center; + white-space: nowrap; + background-color: var(--border-light); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +/* Dark mode specific fixes */ +[data-bs-theme="dark"] .form-control { + background-color: #2d3748; + color: #f9fafb; + border-color: #4b5563; +} + +[data-bs-theme="dark"] .form-control::placeholder { + color: #9ca3af; +} + +/* Fix search bar in dark mode */ +[data-bs-theme="dark"] .search-input-group .form-control, +[data-bs-theme="dark"] .search-input-group .input-group-text { + background-color: #2d3748 !important; + color: #f9fafb !important; + border-color: #4b5563 !important; +} + +/* Search prefix animation */ +.prefix-search-expanded { + animation: expandWidth 0.3s ease-out forwards; +} + +.prefix-search-form { + display: flex; + align-items: center; +} + +.prefix-search-input-group { + display: flex; + width: 250px; +} + +.prefix-search-input-group .form-control { + flex: 1; +} \ No newline at end of file diff --git a/webui/src/styles/components/tables.css b/webui/src/styles/components/tables.css new file mode 100644 index 00000000000..bd2f7b389f9 --- /dev/null +++ b/webui/src/styles/components/tables.css @@ -0,0 +1,38 @@ +/* Tables */ +.table { + width: 100%; + color: var(--text); + border-collapse: separate; + border-spacing: 0; + margin-bottom: 0; + box-shadow: var(--shadow-sm); + border-radius: var(--radius-md); + overflow: hidden; +} + +.table th { + font-weight: 600; + text-align: left; + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 2px solid var(--border); + background-color: var(--background-alt); + color: var(--text-dark); + position: sticky; + top: 0; + z-index: 1; +} + +.table td { + padding: var(--spacing-sm) var(--spacing-md); + border-bottom: 1px solid var(--border-light); + vertical-align: middle; + transition: background-color var(--transition-fast); +} + +.table tr:last-child td { + border-bottom: none; +} + +.table tr:hover { + background-color: var(--border-light); +} \ No newline at end of file diff --git a/webui/src/styles/components/ui-components.css b/webui/src/styles/components/ui-components.css new file mode 100644 index 00000000000..fdfe231baed --- /dev/null +++ b/webui/src/styles/components/ui-components.css @@ -0,0 +1,443 @@ +/* Badges */ +.badge { + display: inline-block; + padding: 0.25em 0.6em; + font-size: 0.75em; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--radius-full); + transition: color var(--transition-fast) ease-in-out, background-color var(--transition-fast) ease-in-out; +} + +.badge-info { + color: white; + background-color: var(--info); +} + +/* Alerts */ +.alert { + position: relative; + padding: var(--spacing-md) var(--spacing-lg); + margin-bottom: var(--spacing-lg); + border: 1px solid transparent; + border-radius: var(--radius); +} + +.alert-info { + color: var(--info-dark); + background-color: var(--color-bg-unknown); + border-color: var(--info-light); +} + +.alert-warning { + color: var(--warning-dark); + background-color: var(--color-bg-changed); + border-color: var(--warning-light); +} + +.alert-danger { + color: var(--danger-dark); + background-color: var(--color-bg-removed); + border-color: var(--danger-light); +} + +/* Dark mode alert fixes */ +[data-bs-theme="dark"] .alert-info { + background-color: #1e3a8a; + color: #bfdbfe; + border-color: #3b82f6; +} + +[data-bs-theme="dark"] .alert-warning { + background-color: #fbbf24; + color: #1f2937; + border-color: #f59e0b; +} + +/* Action Bar */ +.action-bar { + display: flex; + align-items: center; + justify-content: space-between; +} + +.action-bar.borderless { + border-bottom: none; +} + +.action-bar .float-start .btn { + margin-right: var(--spacing-sm); +} + +.action-bar .float-end .btn { + margin-left: var(--spacing-sm); +} +.action-bar .btn { + margin-right: var(--spacing-sm); +} + +.action-bar .btn:last-child { + margin-right: 0; +} + +/* Action Group - Add spacing between buttons */ +.action-group .btn { + margin-right: var(--spacing-sm); +} + +.action-group button:last-child { + margin-right: 0; +} + +/* Fix spacing for PrefixSearchWidget when expanded */ +.action-group form { + margin-right: var(--spacing-sm); +} + +.action-group-right form { + margin-right: var(--spacing-sm); +} + +/* Upload and Import buttons spacing */ +.action-group [class*="Upload"] + [class*="Import"], +.action-group button + button, +button[class*="Upload"] + button[class*="Import"], +[class*="Upload"] ~ [class*="Import"], +[data-testid*="upload"] + [data-testid*="import"], +button[title*="Upload"] + button[title*="Import"], +a[title*="Upload"] + a[title*="Import"] { + margin-left: var(--spacing-xl) !important; + margin-right: var(--spacing-md) !important; +} + +/* LakeFS URI */ +.lakefs-uri { + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--text); + width: 95%; + min-width: 0; + padding: var(--spacing-sm) var(--spacing-md); + background-color: var(--border-light); + border-radius: var(--radius); + display: flex; + align-items: center; + margin-right: var(--spacing-md); +} + +.lakefs-uri .octicon { + color: var(--color-uri-octicon); + margin-right: var(--spacing-xs); +} + +.lakefs-uri a { + color: var(--color-uri-link); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + +.lakefs-uri strong { + margin: 0 5px; +} + +[data-bs-theme="dark"] .lakefs-uri { + background-color: #2d3748; + color: #f9fafb; +} + +/* Getting Started Card */ +.getting-started-card { + margin-top: 64px; + background-color: var(--surface); + border: none; + border-radius: var(--radius-lg); + color: var(--text); + padding: 64px 48px 32px 48px; + box-shadow: var(--shadow-lg); + position: relative; + overflow: hidden; +} + +.getting-started-card > .main-title { + font-style: normal; + font-weight: 300; + font-size: 42px; + line-height: 1.2; + margin-bottom: var(--spacing-lg); + background: linear-gradient(90deg, var(--primary) 0%, var(--success) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.getting-started-card > .text-container { + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-xl); +} + +.getting-started-card > .text-container h4 { + font-size: 20px; + font-weight: 500; + line-height: 1.4; + font-style: normal; + color: var(--text); +} + +.getting-started-card > .text-container p { + font-size: 18px; + font-weight: 300; + line-height: 1.6; + font-style: normal; + color: var(--text-light); +} + +.getting-started-card > .text-container p .sub-title { + font-size: 20px; + font-weight: 300; + line-height: 1.4; + font-style: normal; + color: var(--text); +} + +.getting-started-card > .button-container { + margin-bottom: 64px; +} + +.getting-started-card .learn-more { + font-size: 16px; + line-height: 24px; + font-weight: 400; + font-style: normal; + color: var(--text-light); +} + +.getting-started-card .getting-started-image { + width: 50%; + position: absolute; + right: 0; + bottom: 0; + opacity: 0.9; + transform: translateY(20%); + transition: transform var(--transition-slow), opacity var(--transition-slow); +} + +.getting-started-card:hover .getting-started-image { + transform: translateY(15%); + opacity: 1; +} + +.create-sample-repo-button { + padding: 12px 24px; + font-size: 16px; + line-height: 24px; + font-weight: 500; + border-radius: var(--radius); + transition: all var(--transition); + background-color: var(--primary); + color: white; + border: none; + box-shadow: var(--shadow); +} + +.create-sample-repo-button:hover { + background-color: var(--primary-dark); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.create-sample-repo-button.btn-link { + background: none; + color: var(--primary); + box-shadow: none; + padding: 0; +} + +.create-sample-repo-button.btn-link:hover { + color: var(--primary-dark); + background: none; + text-decoration: underline; + transform: none; + box-shadow: none; +} + +/* SyntaxHighlighter styling */ +.react-syntax-highlighter { + border-radius: var(--radius-md) !important; + margin: var(--spacing-md) 0 !important; + font-family: var(--font-mono) !important; + font-size: 0.9rem !important; + line-height: 1.5 !important; + background-color: var(--background-alt) !important; + border: 1px solid var(--border) !important; + box-shadow: var(--shadow-sm) !important; +} + +/* Fix code background in SyntaxHighlighter */ +.react-syntax-highlighter code, +.react-syntax-highlighter pre { + background-color: transparent !important; + border: none !important; + font-family: var(--font-mono) !important; + padding: 0 !important; +} + +/* SyntaxHighlighter line numbers */ +.react-syntax-highlighter .linenumber { + color: var(--text-light) !important; + border-right: 1px solid var(--border) !important; + padding-right: var(--spacing-sm) !important; + margin-right: var(--spacing-sm) !important; + min-width: 3em !important; + text-align: right !important; + display: inline-block !important; /* Fix alignment issues */ + width: 3em !important; /* Ensure consistent width */ +} + +/* SyntaxHighlighter dark mode */ +[data-bs-theme="dark"] .react-syntax-highlighter { + background-color: #1e293b !important; + border-color: #1e293b !important; /* Remove light border in dark mode */ +} + +[data-bs-theme="dark"] .react-syntax-highlighter .linenumber { + color: #64748b !important; + border-right-color: #374151 !important; +} + +/* SyntaxHighlighter token colors for light mode */ +.react-syntax-highlighter .hljs-keyword { + color: #8250df !important; +} + +.react-syntax-highlighter .hljs-string { + color: #0a3069 !important; +} + +.react-syntax-highlighter .hljs-comment { + color: #6e7781 !important; + font-style: italic !important; +} + +.react-syntax-highlighter .hljs-function { + color: #953800 !important; +} + +.react-syntax-highlighter .hljs-number { + color: #0550ae !important; +} + +.react-syntax-highlighter .hljs-operator { + color: #0550ae !important; +} + +.react-syntax-highlighter .hljs-built_in { + color: #e36209 !important; +} + +.react-syntax-highlighter .hljs-tag { + color: #116329 !important; +} + +.react-syntax-highlighter .hljs-attr { + color: #0550ae !important; +} + +/* SyntaxHighlighter token colors for dark mode */ +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-keyword { + color: #c792ea !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-string { + color: #a5d6ff !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-comment { + color: #8b949e !important; + font-style: italic !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-function { + color: #d2a8ff !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-number { + color: #79c0ff !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-operator { + color: #79c0ff !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-built_in { + color: #ffa657 !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-tag { + color: #7ee787 !important; +} + +[data-bs-theme="dark"] .react-syntax-highlighter .hljs-attr { + color: #79c0ff !important; +} + +/* GetStarted Component Styles */ +.get-started-container { + background-color: var(--background-alt); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + padding: 0.5rem; + text-align: center; +} + +.get-started-image { + max-height: 250px; + transition: transform var(--transition), opacity var(--transition); +} + +.get-started-container:hover .get-started-image { + transform: scale(1.1); +} + +.get-started-card { + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border); + transition: transform var(--transition), box-shadow var(--transition); +} + +.get-started-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); + border-color: var(--primary-light); +} + +.get-started-icon-container { + background-color: var(--border-light); + border-radius: 50%; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Dark mode adjustments */ +[data-bs-theme="dark"] .get-started-container { + background-color: var(--surface); +} + +[data-bs-theme="dark"] .get-started-icon-container { + background-color: rgba(59, 130, 246, 0.1); +} + +/* Bootstrap popover size */ +.bs-popover-max-width { + max-width: 400px !important; +} +.popover { + max-width: 400px !important; +} \ No newline at end of file diff --git a/webui/src/styles/globals.css b/webui/src/styles/globals.css index 9952dddbb60..6be17082668 100644 --- a/webui/src/styles/globals.css +++ b/webui/src/styles/globals.css @@ -1,928 +1,228 @@ :root { - --color-fg-added: var(--bs-success); - --color-fg-removed: var(--bs-danger); - --color-fg-changed: var(--bs-warning); - --color-fg-conflict: var(--bs-secondary); - --color-uri-octicon: #0c5460; - --color-uri-link: #0f6674; - --color-bg-added: #e6ffec; - --color-bg-removed: #ffece6; - --color-bg-changed: #fff6db; - --color-bg-unknown: #f1f4ff; + /* Modern Bright Color Palette */ + --primary: #3b82f6; + --primary-light: #60a5fa; + --primary-dark: #2563eb; + --secondary: #ec4899; + --secondary-light: #f472b6; + --secondary-dark: #db2777; + --success: #10b981; + --success-light: #34d399; + --success-dark: #059669; + + /* Bootstrap variable overrides */ + --bs-nav-pills-link-active-bg: #3b82f6; + --bs-nav-pills-link-active-color: white; + --bs-nav-link-color: #3b82f6; + --bs-nav-link-hover-color: #2563eb; + --danger: #ef4444; + --danger-light: #f87171; + --danger-dark: #dc2626; + --warning: #f59e0b; + --warning-light: #fbbf24; + --warning-dark: #d97706; + --info: #06b6d4; + --info-light: #22d3ee; + --info-dark: #0891b2; + --background: #ffffff; + --background-alt: #f9fafb; + --surface: #ffffff; + --text: #1f2937; + --text-light: #6b7280; + --text-dark: #111827; + --border: #e5e7eb; + --border-light: #f3f4f6; + --border-dark: #d1d5db; + + /* Semantic Colors */ + --color-fg-added: var(--success); + --color-fg-removed: var(--danger); + --color-fg-changed: var(--warning); + --color-fg-conflict: var(--secondary); + --color-uri-octicon: var(--primary); + --color-uri-link: var(--primary-dark); + --color-bg-added: #ecfdf5; + --color-bg-removed: #fef2f2; + --color-bg-changed: #fffbeb; + --color-bg-unknown: #eff6ff; --color-bg-conflict: #fff3cd; - --color-policy-text: #d63384; -/* The following is all the colors that are used by the css (kept here for refernce, when adding new colors): */ -/* --bs-success: var(--bs-success);*/ -/* --bs-danger: var(--bs-danger);*/ -/* --bs-warning: var(--bs-warning);*/ -/* --bs-body-bg: var(--bs-body-bg);*/ -/* --bs-tertiary-bg: var(--bs-tertiary-bg);*/ -/* --bs-light-border-subtle: var(--bs-light-border-subtle);*/ -/* --bs-border-color: var(--bs-border-color);*/ -/* --bs-dark-border-subtle: var(--bs-dark-border-subtle);*/ -/* --bs-secondary: var(--bs-secondary);*/ -/* --bs-light-text-emphasis: var(--bs-light-text-emphasis);*/ -/* --bs-dark-text-emphasis: var(--bs-dark-text-emphasis);*/ -/* --bs-body-color: var(--bs-body-color);*/ -/* --bs-emphasis-color: var(--bs-emphasis-color);*/ -/* --bs-blue: var(--bs-blue);*/ -/* --bs-cyan: var(--bs-cyan);*/ -/* --bs-pink: var(--bs-pink);*/ -/* --bs-teal: var(--bs-teal);*/ + --color-policy-text: #be185d; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + /* Border Radius */ + --radius-sm: 0.25rem; + --radius: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-full: 9999px; + + /* Typography */ + --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + + /* Transitions */ + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); } [data-bs-theme="dark"] { - --color-policy-text: #ffb6c1; - --color-uri-octicon: #f3ab9f; - --color-uri-link: #f0998b; - --color-bg-added: #002008; - --color-bg-removed: #571500; - --color-bg-changed: #6b2900; - --color-bg-unknown: #00104a; - --color-bg-conflict: #32001c; -} - -.gray-out { - background-color: rgba(255, 255, 255, 0.5); -} - -.overlay { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - -.main-app { - margin: 15px; -} - - -.logo { - height: 30px; -} - -.navbar-brand { + --primary: #60a5fa; + --primary-light: #93c5fd; + --primary-dark: #3b82f6; + --secondary: #f472b6; + --secondary-light: #fb7185; + --secondary-dark: #ec4899; + --success: #34d399; + --success-light: #6ee7b7; + --success-dark: #10b981; + + /* Bootstrap variable overrides for dark mode */ + --bs-nav-pills-link-active-bg: #60a5fa; + --bs-nav-pills-link-active-color: white; + --bs-nav-link-color: #60a5fa; + --bs-nav-link-hover-color: #93c5fd; + --danger: #f87171; + --danger-light: #fca5a5; + --danger-dark: #ef4444; + --warning: #fbbf24; + --warning-light: #fcd34d; + --warning-dark: #f59e0b; + --info: #22d3ee; + --info-light: #67e8f9; + --info-dark: #06b6d4; + --background: #111827; + --background-alt: #1f2937; + --surface: #1f2937; + --text: #f9fafb; + --text-light: #e5e7eb; + --text-dark: #f3f4f6; + --border: #374151; + --border-light: #4b5563; + --border-dark: #6b7280; + + --color-policy-text: #f9a8d4; + --color-uri-octicon: #60a5fa; + --color-uri-link: #93c5fd; +} + + +/* Fix code elements in dark mode */ +[data-bs-theme="dark"] code { + background-color: #374151; + color: #e5e7eb; + border: 1px solid #4b5563; +} + + +[data-bs-theme="dark"] .bg-light { + background-color: #2d3748 !important; +} + +/* Global Styles */ +body { + font-family: var(--font-sans); + color: var(--text); + background-color: var(--background-alt); + line-height: 1.5; + margin: 0; padding: 0; } -.navbar > div { - padding-top: 7px; +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.25; + margin-bottom: 1rem; } -.card.login-widget {} - -.card.login-widget .btn { - width: 100%; -} - -.card.login-widget,.card.setup-widget, .reset-pwd-widget, .request-reset-pwd-widget { - margin-top: 130px; -} - -.card.create-invited-user-widget { - margin-bottom: 10px; -} - -.action-bar { - padding-bottom: 10px; - margin-bottom: 10px; -} - -.action-bar.borderless { - border-bottom: none; -} - -.action-bar .float-start .btn { - margin-right: 10px; -} - -.action-bar .float-end .btn { - margin-left: 10px; -} - -.dismiss-btn { - padding-left: 2px; - padding-right: 2px; -} - -.navbar a.dropdown-toggle { - color: var(--bs-white); -} -.navbar a.dropdown-toggle:hover { - color: var(--bs-border-color); -} - -.nav-tabs .nav-link.active { - font-weight: bold; -} - -.commit-list .list-group-item:nth-of-type(even) { - background-color: var(--bs-tertiary-bg); -} - -.commit-actions .btn, .branch-actions .btn, .run-commit { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: .8rem; - vertical-align: center; -} - -.lakefs-uri .octicon { - color: var(--color-uri-octicon); - margin-right: 5px; -} - -.lakefs-uri a { - color: var(--color-uri-link); - text-decoration: underline; -} - -.lakefs-uri { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: .9rem; - color: var(--bs-body-color); - width: 95%; - min-width: 0; -} - -.lakefs-uri strong { - margin: 0 5px; -} - -/* ref dropdown */ -.ref-popover .popover-body { - padding:0; -} - -.ref-popover, .ref-popover .btn { - font-size: 0.8rem; - line-height: 0.8rem; -} - -.ref-popover { - max-width: 650px; - min-width: 450px; -} - -.ref-scroller { - max-height: 250px; - overflow: auto; - min-height: 30px; -} - -.ref-list .list-group-item:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.ref-list .list-group-item { - padding: 10px 15px; - border: 0; - border-bottom: 1px solid var(--bs-border-color); - border-radius: 0; - clear: both; -} -.ref-list .list-group-item .actions { - float: right; -} -.ref-list .list-group-item .actions .badge { - margin-right: 5px; - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; -} - -.ref-popover h5 { - padding: 10px; - border-bottom: 1px solid var(--bs-border-color); - background-color: var(--bs-tertiary-bg); - text-align: center; -} - -.ref-filter-form { - padding: 10px; - border-bottom: 1px solid var(--bs-border-color); - background-color: var(--bs-tertiary-bg); -} -.ref-filter-form form { - margin-bottom: 0; -} -.ref-paginator { - text-align: center; - padding: 10px 0; -} -/* end ref dropdown */ - - -/* tree table */ -.diff-indicator { - width: 50px; -} - -.diff-indicator span { - padding: 0 10px; -} - -.table .leaf-entry-row { - border: 1px solid rgba(0,0,0,.125); -} - -.table .leaf-entry-row .objects-diff .stats-diff-block { - padding: 2px; -} - -.table .leaf-entry-row .objects-diff .removed { - color: var(--color-fg-removed); - font-weight: bold; -} - -.table .leaf-entry-row .objects-diff .added { - color: var(--color-fg-added); - font-weight: bold; -} - -.table .leaf-entry-row .objects-diff .unchanged { - color: var(--color-fg-conflict); - font-weight: bold; -} - -.table .leaf-entry-row .objects-diff .conflict { - color: var(--color-fg-conflict); - font-weight: bold; -} - -.table .leaf-entry-row .objects-diff .diff-size { - font-weight: bold; -} - -.table .tree-entry-row, -.table .change-entry-row { - border-bottom: 1px solid rgba(0, 0, 0, 0.125); - height: 38px; -} - -.table .tree-entry-row:last-child, -.table .change-entry-row:last-child { - border-bottom: 0; -} - - -.table .change-entry-row:hover { - background-color: var(--bs-tertiary-bg); -} - -.table .tree-entry-row td, -.table .change-entry-row td, -.actions-runs-list .table td { - padding-top: 5px; - padding-bottom: 5px; - vertical-align: middle; -} - -#run-details { - margin-bottom: 20px; -} - -.table .tree-entry-row td:first-child, -.table .change-entry-row td:first-child, -.table .change td:first-child { - padding-left: 20px; -} - -.table .tree-entry-row td:nth-child(4), -.table .change-entry-row td:nth-child(4) { - padding-right: 20px; - text-align: right; -} - -.table .change-entry-row td:last-child, -.table .tree-entry-row td:last-child { - padding-right: 10px; - text-align: right; -} - -.tree-path .octicon { - color: var(--bs-blue); - margin-right: 5px; - font-size: .875rem; -} - -.diff-removed .tree-path, -.diff-removed .tree-path .octicon, -.diff-removed .diff-indicator .octicon { - color: var(--bs-secondary); -} - -td.tree-path { - padding: 0px; - text-align: left; - width: 30%; -} - -.diff-changed td { - background-color: var(--color-bg-changed); -} - -.diff-added td { - background-color: var(--color-bg-added) -} - -.diff-removed td { - background-color: var(--color-bg-removed); -} - -.diff-conflict td { - background-color: var(--color-bg-conflict); -} - -.diff-more td { - background-color: var(--bs-body-bg); -} - -.table tr .row-hover { - visibility: hidden; - font-size: 0.7em; -} - -.table tr:hover .row-hover { - visibility: visible; - display: inline-block; -} - -.view-options label { - margin-bottom: 0; - padding: 5px; - font-size: smaller; -} - -.view-options [type='radio'] { - display: none; -} - -.hook-logs { - background: var(--bs-body-color); - color: var(--bs-tertiary-bg); - padding: 40px 10px; -} -.hook-log, .hook-log pre { - background: var(--bs-body-color); - color: var(--bs-tertiary-bg); -} -.hook-log pre { - padding: 20px 0; -} -.hook-log-content { - padding: 0 0 0 20px; -} -.hook-log-title { - font-size: 1.2em; -} -.hook-log-title small { - font-size: 0.7em; - color: var(--bs-dark-border-subtle); - margin-left: 10px; -} - -.hook-log-title .btn, .hook-log-title .btn.btn-link { - color: var(--bs-tertiary-bg); -} - -.change-entry-row-actions { - padding:0; -} - -.table .change-entry-row .change-entry-row-actions .btn-link, -.table .tree-entry-row .change-entry-row-actions .btn-link .btn-link:hover { - visibility: hidden; - font-size: 0.875rem; - padding: 5px; -} -.table .change-entry-row:hover .change-entry-row-actions .btn-link, -.table .tree-entry-row .change-entry-row-actions .btn-link, -.table .tree-entry-row:focus .change-entry-row-actions .btn-link { - visibility: visible; - display: inline-block; -} - -td.change-entry-row-actions { - width: 12%; -} - -.tree-container { - font-size: .9rem; -} - -.tree-container .card .card-header { - padding: 10px; -} - -.tree-container .card .card-body { - padding: 0; -} - -.tree-container .card .card-body > .table { - margin-bottom: 0; -} - -.tree-container .btn.btn-link { - font-size: .9rem; -} - -.tree-container .table { - margin-bottom: 0; -} - -.tree-container .table td { - word-break: break-word; -} - -.commit-metadata { - font-size: 0.85rem; -} - -.branch-table td { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; -} - -.list-group.pagination-group { - margin-bottom: 30px; -} - -.policy-body { - padding: 30px; - background-color: var(--bs-light-border-subtle); -} - -.auth-page .tab-content { - padding: 20px 0; -} - -textarea.form-control.policy-document { - font-family: "SFMono-Regular", Menlo, Consolas, Monospace, serif; - font-size: 10px; - color: var(--color-policy-text); - background-color: var(--bs-tertiary-bg); - padding: 10px; -} -textarea.form-control.policy-document:focus { - font-family: "SFMono-Regular", Menlo, Consolas, Monospace, serif; - font-size: 10px; - color: var(--color-policy-text); - padding: 10px; -} - -.input-group>.input-group-prepend>.input-group-text { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -td .form-group { - margin-bottom: 0; -} - -.section-title { - padding-bottom: 5px; - padding-top: 10px; - margin-bottom: 15px; - border-bottom: 1px solid rgba(0,0,0,.125); - overflow: hidden; -} - -.btn { - text-transform: none; -} -.btn.btn-link, .btn.btn-link:hover, .btn.btn-link:not(.disabled):hover { - padding: 0; - margin-top: 0; - border-bottom-width: 0; -} - -.auth-learn-more { - margin-top: -20px; - margin-bottom: 20px; -} - -.tree-column{ - width: 2px; -} - -.change-summary .arrow { - visibility: hidden; -} - -td.change-summary { - padding: 0px; - text-align: right; - width: 10%; -} - -.color-fg-added { - color: var(--color-fg-added); - font-weight: bold; -} - -.color-fg-removed { - color: var(--color-fg-removed); - font-weight: bold; -} - -.color-fg-changed { - color: var(--color-fg-changed); - font-weight: bold; -} - -.color-fg-conflict { - color: var(--color-fg-conflict); - font-weight: bold; -} - -.change-summary-icon { - margin-left: 0.125rem; - padding: .1rem; -} - -.reset-req-sent { - align-items: center; - text-align: center; - margin-top: 50%; - font-size: 18px; -} - -.invited-welcome-msg { - align-items: center; - padding-top: 200px; - padding-bottom: 0px; -} - -.invited-welcome-msg .title { - text-align: left; - padding-bottom: 30px; - font-size: 30px; - font-weight: bold; - color: var(--bs-cyan); -} - -.invited-welcome-msg .body { - text-align: left; - font-size: 20px; - padding-bottom: 40px; -} - -.action-bar .btn { - border: transparent; -} - -.action-group-right .btn { - margin-left: 5px; -} - -.action-group-left .btn { - margin-right: 5px; -} - -.action-bar .btn.create-user { - background-color: var(--bs-success); -} - -.action-bar .btn.cancel-create-user { - background-color: var(--bs-dark-text-emphasis); -} - - - -.after-setup-btn { - width: 250px; - margin-top: 7px; - background-color: var(--bs-success); - color: var(--bs-body-bg); - border: 0; -} -.after-setup-btn:hover { - color: var(--bs-body-bg); - background-color: var(--bs-light-text-emphasis); -} - -.after-setup-card code { - color: var(--bs-light-text-emphasis); -} - -.copy-button { - display: inline-flex; - align-items: center; - height: 100%; - padding: 0 5px 0; -} - -.import-num-objects { - display: inline-flex; - color: var(--bs-success); - font-weight: bolder; - margin: 0 2px; -} - -.import-text { - display: flex; - justify-content: center -} - -.import-success { - color: var(--bs-success); - display: flex; - justify-content: center; - font-size: 160% -} - -.wizard-progress-bar { - margin-bottom: 25px; -} - -.wizard-step-header { - display: flex; - justify-content: left; - margin-bottom: 20px; - margin-top: 40px; -} - -.code-container { - border-radius: 10px; - border: 1px solid var(--bs-border-color); - background-color: var(--bs-tertiary-bg); -} - -.file-content-card { - margin-top: 0; -} - -.file-content-heading { - background-color: var(--bs-tertiary-bg); - padding: 10px; -} - -.file-content-body { - padding: 0; -} - - -.form-label { - white-space: nowrap; -} - -.download-button { - margin-right: 8px; -} - - -.object-viewer-sql-input.form-control { - background-color: var(--bs-light-text-emphasis); - font-size: .8rem; - font-family: "SFMono-Regular", menlo, consolas, monospace; - color: var(--bs-dark-border-subtle); -} -.object-viewer-sql-input.form-control:focus { - background-color: var(--bs-light-text-emphasis); - color: var(--bs-body-bg); -} - -.powered-by { - line-height: .8em; - font-size: .8em; - font-style: italic; - -} - -.object-viewer-sql-results { - overflow: auto; - font-size: .75rem; - max-height: 400px; -} -.object-viewer-sql-results th { - font-size: .9rem; - padding: 5px 10px; -} -.object-viewer-sql-results th small { - font-size: .7rem; - color: var(--bs-dark-border-subtle); -} - -.image-container { - padding: 30px; -} -.image-container img { - max-width: 100%; -} -.object-viewer-pdf { - margin: 20px; -} -.object-viewer-pdf object { - width: 100%; - height: 600px; -} -.object-viewer-markdown { - padding: 20px; -} -.object-viewer-markdown h2, -.object-viewer-markdown h3, -.object-viewer-markdown h4, -.object-viewer-markdown h5, -.object-viewer-markdown h6 { - padding-bottom: 0.25em; - margin-bottom: 1.25rem; - border-bottom: 1px solid var(--bs-border-color); - margin-top: 2rem; -} - -.required-field-label { - color: var(--bs-danger); -} - -.otf-diff-update { - background-color: var(--color-bg-changed); -} - -.otf-diff-create { - background-color: var(--color-bg-added) -} - -.otf-diff-delete { - background-color: var(--color-bg-removed); -} - -.otf-diff-unknown { - background-color: var(--color-bg-unknown); -} - -.table.table-diff { - table-layout: fixed; -} - -td.table-operation-type { - padding-left: 30px; -} - -td.table-id, td.table-id-placeholder { - text-align: left; - width: 8%; -} - -td.operation-expansion, td.operation-expansion-placeholder { - padding-right: 0px; - width: 4%; -} - -td.table-operation-details { - padding-left: 30px; -} - -.table-operation-expansion { - color: var(--bs-emphasis-color); -} - -td.table-diff-type { - padding: 10px; - font-size: medium; -} - -td.entry-type-indicator { - width: 1%; - padding: 2px; +a { + color: var(--primary); + text-decoration: none; + transition: color var(--transition-fast); } -.user-menu-notification-indicator { - top: 8px; - left: 10px; - position: relative; - background: linear-gradient(#54a3ff, #006eed); - border-radius: 50%; - color: var(--bs-body-bg); - height: 10px; - line-height: 20px; - margin-left: 5px; - width: 10px; -} -.menu-item-notification-indicator { - right: 10px; - position: relative; - display: inline-block; - background: linear-gradient(#54a3ff, #006eed); - border-radius: 50%; - color: var(--bs-body-bg); - height: 10px; - line-height: 20px; - margin-left: 5px; - width: 10px; +a:hover { + color: var(--primary-dark); } -.getting-started-card { - margin-top: 64px; - background-color: var(--bs-tertiary-bg); - border: none; - color: var(--bs-body-color); - padding: 104px 72px 32px 72px; - font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +code { + font-family: var(--font-mono); + font-size: 1em; + background-color: var(--border-light); + padding: 0.2em 0.4em; + border-radius: var(--radius-sm); } -.getting-started-card > .main-title { - font-style: normal; - font-weight: 300; - font-size: 48px; - line-height: 64px; +/* Layout */ +.main-app { + padding: 75px 0 var(--spacing-lg) 0; } -.getting-started-card > .text-container { - margin-top: 8px; - margin-bottom: 48px; -} -.getting-started-card > .text-container h4 { - font-size: 20px; - font-weight: 500; - line-height: 32px; - font-style: normal; - color: var(--bs-body-color); +/* Fix for btn-light in dark mode */ +[data-bs-theme="dark"] .btn-light { + background-color: #374151; + color: #f9fafb; + border-color: #4b5563; } -.getting-started-card > .text-container p { - font-size: 20px; - font-weight: 200; - line-height: 32px; - font-style: normal; - color: var(--bs-body-color); +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } } -.getting-started-card > .text-container p .sub-title { - font-size: 20px; - font-weight: 300; - line-height: 32px; - font-style: normal; - color: var(--bs-body-color); +@keyframes expandWidth { + from { + width: 0; + opacity: 0; + } + to { + width: 100%; + opacity: 1; + } } -.getting-started-card > .button-container { - margin-bottom: 104px; +.fade-in { + animation: fadeIn var(--transition-slow); } -.getting-started-card .learn-more { - font-size: 16px; - line-height: 24px; - font-weight: 400; - font-style: normal; -} -.getting-started-card .getting-started-image { - width: 665px; +/* Utilities */ +.overlay { position: absolute; + top: 0; + bottom: 0; + left: 0; right: 0; - bottom: -43px; -} - -.create-sample-repo-button { - width: 256px; - height: 56px; - font-size: 16px; - line-height: 24px; -} - -.create-sample-repo-button.btn-link { - text-decoration: none; -} - -.create-sample-repo-button.btn-link:hover { - background: none; -} - -.file-drop-zone { - text-align: center; - background-color: var(--bs-light-border-subtle); - height: 80px; - vertical-align: center; - line-height: 80px; - border: 2px dashed var(--bs-dark-border-subtle); - color: var(--bs-secondary); - font-size: 1.3em; - cursor: pointer; - margin-bottom: 20px; -} - -.file-drop-zone.file-drop-zone-focus { - border: 2px dashed #94B49F; - color: #94B49F; - background-color: var(--bs-light-border-subtle); -} - -.upload-item { - font-family: "SFMono-Regular", menlo, consolas, monospace; - padding: 3px 0; - font-size: .75em; -} - -.upload-item-uploading { - background-color: var(--bs-light-border-subtle); -} - -.upload-item-done { - color: var(--bs-success); + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; } -.pull-details .description { - min-height: 160px; +.gray-out { + opacity: 0.5; + pointer-events: none; } .geojson-map-wrapper { diff --git a/webui/src/styles/navigation/navigation.css b/webui/src/styles/navigation/navigation.css new file mode 100644 index 00000000000..2a36a321156 --- /dev/null +++ b/webui/src/styles/navigation/navigation.css @@ -0,0 +1,201 @@ +/* Layout: Breadcrumb */ +.main-app .breadcrumb { + padding: var(--spacing-sm) var(--spacing-md) +} + +/* Navbar */ +.navbar { + background-color: var(--surface); + box-shadow: var(--shadow); + padding: var(--spacing-sm) var(--spacing-lg); + border-bottom: 1px solid var(--border); + position: fixed; /* Make navbar fixed */ + top: 0; /* Position at the top */ + left: 0; /* Align to the left */ + right: 0; /* Stretch to the right */ + z-index: 1050; /* Ensure it stays on top */ +} + +.navbar-brand { + padding: 0; + display: flex; + align-items: center; +} + +.logo { + height: 30px; +} + +.navbar a.dropdown-toggle { + color: var(--text); +} + +.navbar a.dropdown-toggle:hover { + color: var(--primary); +} + +/* Fix for dark navbar with dark text */ +.navbar-dark .navbar-nav .nav-link, +.bg-dark .nav-link, +.bg-dark .dropdown-toggle { + color: white !important; +} + +/* Tabs - Modern Style */ +.nav-tabs { + border-bottom: none; + margin-bottom: 0; + margin-left: var(--spacing-md); + display: flex; + gap: var(--spacing-md); +} + +.nav-tabs .nav-link { + margin-bottom: 0; + border: none; + border-radius: var(--radius-sm) var(--radius-sm) 0 0; + color: var(--text-light); + padding: var(--spacing-sm) var(--spacing-lg); + transition: all var(--transition), + transform var(--transition-fast), + box-shadow var(--transition), + scale var(--transition-fast); + position: relative; + font-weight: 500; + background-color: transparent; + border-bottom: 1px solid var(--border); +} + +.nav-tabs .nav-link:hover { + color: var(--primary); + background-color: rgba(59, 130, 246, 0.05); + transform: translateY(-2px); + scale: 1.03; +} + +.nav-tabs .nav-link.active { + color: var(--primary); + background-color: var(--background-alt); + font-weight: 600; + border-bottom: 2px solid var(--primary); + transform: translateY(-2px); + position: relative; + scale: 1.05; +} + +.nav-tabs .nav-link.active::before { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 2px; + background-color: var(--primary); + opacity: 0.7; + transition: opacity var(--transition); +} + +.nav-tabs .nav-link.active::after { + content: none; /* Remove the after pseudo-element since we're using border-bottom now */ +} + + +/* Nav pills styling */ +.nav-pills { + --bs-nav-pills-link-active-bg: var(--primary); + --bs-nav-pills-link-active-color: white; + --bs-nav-link-color: var(--primary); + --bs-nav-link-hover-color: var(--primary-dark); +} + + +/* Repository tabs specific styling */ +.repository-tabs .nav-tabs, +.nav-tabs[role="tablist"] { + background-color: var(--background-alt); + padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm); + border-top-left-radius: var(--radius-md); + border-top-right-radius: var(--radius-md); + position: relative; +} + +/* Add transition for the border-bottom */ +.nav-tabs .nav-link { + border-bottom: 2px solid transparent; + transition: color var(--transition), + background-color var(--transition), + transform var(--transition-fast), + box-shadow var(--transition), + border-bottom-color var(--transition); +} + +/* Full-width border for repository navigation */ +.nav-tabs-container { + position: relative; + width: 100%; + border-bottom: 1px solid var(--border); +} + +/* Add this class to the parent element of nav-tabs */ +.full-width-tabs-border { + position: relative; + width: 100%; + overflow: hidden; +} + +.full-width-tabs-border::after { + content: ''; + position: absolute; + bottom: 0; + left: -100vw; + right: -100vw; + height: 1px; + background-color: var(--border); + z-index: 1; +} + +.full-width-tabs-border .nav-item a { + border: 0; + border-bottom: 0; + padding-bottom: var(--spacing-sm); + transition: color var(--transition), + background-color var(--transition), + transform var(--transition-fast), + box-shadow var(--transition); +} + +.full-width-tabs-border .nav-item a:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-sm); +} + +.full-width-tabs-border .nav-item a.active { + background-color: var(--background-alt); + position: relative; + z-index: 2; /* Ensure active tab appears above the border */ + transform: translateY(-2px); + box-shadow: var(--shadow-sm); +} + +/* Pagination */ +.pagination-group { + display: flex; + justify-content: center; + margin-top: var(--spacing-lg); + margin-bottom: var(--spacing-lg); +} + +/* Fix branch selector width */ +.dropdown-menu { + min-width: 250px !important; + width: auto !important; + z-index: 1050 !important; /* Ensure dropdowns appear above other elements */ +} + +/* Branch selector specific styles */ +[class*="branch"] .dropdown-menu, +.dropdown-menu[aria-labelledby*="branch"], +div[class*="branch-selector"] .dropdown-menu { + min-width: 500px !important; + width: auto !important; +} \ No newline at end of file diff --git a/webui/src/styles/objects/diff.css b/webui/src/styles/objects/diff.css new file mode 100644 index 00000000000..45e0a3702a6 --- /dev/null +++ b/webui/src/styles/objects/diff.css @@ -0,0 +1,106 @@ +/* Diff Styling */ +.diff-changed td { + background-color: var(--color-bg-changed); +} + +.diff-added td { + background-color: var(--color-bg-added); +} + +.diff-removed td { + background-color: var(--color-bg-removed); +} + +.diff-conflict td { + background-color: var(--color-bg-conflict); +} + +/* Improve link contrast on colored backgrounds */ +.diff-changed a, .diff-changed .btn-link { + color: #000000; + font-weight: 600; +} + +.diff-added a, .diff-added .btn-link { + color: #003300; + font-weight: 600; +} + +.diff-removed a, .diff-removed .btn-link { + color: #330000; + font-weight: 600; +} + +.diff-conflict a, .diff-conflict .btn-link { + color: #330033; + font-weight: 600; +} + +[data-bs-theme="dark"] tr.diff-changed > td, +[data-bs-theme="dark"] tr.diff-changed > td a, +[data-bs-theme="dark"] tr.diff-added > td, +[data-bs-theme="dark"] tr.diff-added > td a, +[data-bs-theme="dark"] tr.diff-removed > td, +[data-bs-theme="dark"] tr.diff-removed > td a, +[data-bs-theme="dark"] tr.diff-conflict > td, +[data-bs-theme="dark"] tr.diff-conflict > td a { + color: var(--surface); +} + +/* Ensure hover states maintain contrast */ +.diff-changed a:hover, .diff-changed .btn-link:hover { + color: #333333; +} + +.diff-added a:hover, .diff-added .btn-link:hover { + color: #006600; +} + +.diff-removed a:hover, .diff-removed .btn-link:hover { + color: #660000; +} + +.diff-conflict a:hover, .diff-conflict .btn-link:hover { + color: #660066; +} + +[data-bs-theme="dark"] .diff-changed a:hover, +[data-bs-theme="dark"] .diff-changed .btn-link:hover { + color: #dddddd; +} + +[data-bs-theme="dark"] .diff-added a:hover, +[data-bs-theme="dark"] .diff-added .btn-link:hover { + color: #88ff88; +} + +[data-bs-theme="dark"] .diff-removed a:hover, +[data-bs-theme="dark"] .diff-removed .btn-link:hover { + color: #ff8888; +} + +[data-bs-theme="dark"] .diff-conflict a:hover, +[data-bs-theme="dark"] .diff-conflict .btn-link:hover { + color: #ff88ff; +} + +/* Diff status color classes */ +.color-fg-added { + color: var(--color-fg-added); + font-weight: 600; +} + +.color-fg-removed { + color: var(--color-fg-removed); + font-weight: 600; +} + +.color-fg-changed { + color: var(--color-fg-changed); + font-weight: 600; +} + +.color-fg-conflict { + color: var(--color-fg-conflict); + font-weight: 600; +} \ No newline at end of file diff --git a/webui/src/styles/objects/object-viewer.css b/webui/src/styles/objects/object-viewer.css new file mode 100644 index 00000000000..eaaf61d266b --- /dev/null +++ b/webui/src/styles/objects/object-viewer.css @@ -0,0 +1,140 @@ +/* Object viewer styles */ +.object-viewer-buttons { + display: flex; + align-items: center; +} + +.object-viewer-buttons .btn { + margin-right: var(--spacing-md); +} + +.object-viewer-buttons .btn:last-child { + margin-right: 0; +} + +/* Make images in object viewer scale to fit their container */ +.object-viewer img, +[class*="object-viewer"] img, +[class*="fileViewer"] img, +.file-content-body img { + max-width: 100%; + height: auto; + object-fit: contain; +} + +.object-viewer-pdf object { + width: 100%; + height: 600px; +} + +/* Data Table Specific Styling */ +.object-viewer-sql-results { + margin-top: var(--spacing-md); + border-radius: var(--radius-md); + overflow: auto; + height: 70vh; /* Fixed height instead of max-height to ensure sticky headers work */ + box-shadow: var(--shadow-md); + position: relative; /* Needed for sticky positioning context */ +} + +.object-viewer-sql-results .table { + margin-bottom: 0; + border: 1px solid var(--border); +} + +.object-viewer-sql-results .table-dark th { + background-color: var(--surface); + color: var(--text); + border: none; + border-bottom: 2px solid var(--primary); + font-family: var(--font-mono); + font-size: 0.9rem; + padding: var(--spacing-md) var(--spacing-lg); + position: sticky; + top: 0; + z-index: 10; +} + +.object-viewer-sql-results .table { + border-collapse: collapse; +} + +.object-viewer-sql-results .table th, +.object-viewer-sql-results .table td { + border-left: 1px solid var(--border); + border-right: 1px solid var(--border); +} + +.object-viewer-sql-results .table th:first-child, +.object-viewer-sql-results .table td:first-child { + border-left: 1px solid var(--border); +} + +.object-viewer-sql-results .table th:last-child, +.object-viewer-sql-results .table td:last-child { + border-right: 1px solid var(--border); +} + +.object-viewer-sql-results .table-dark th small { + color: var(--text-light); + font-size: 0.75rem; + display: block; + margin-top: 0.25rem; + font-weight: normal; +} + +.object-viewer-sql-results tbody tr:nth-child(odd) { + background-color: var(--background-alt); +} + +.object-viewer-sql-results tbody tr:hover { + background-color: rgba(59, 130, 246, 0.05); +} + +.object-viewer-sql-results td { + font-size: 0.9rem; + padding: var(--spacing-sm) var(--spacing-md); + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.object-viewer-sql-results td.number-cell { + text-align: right; + font-family: var(--font-mono); +} + +.object-viewer-sql-results td.string-cell { + font-family: var(--font-sans); +} + +.object-viewer-sql-results td.date-cell { + font-family: var(--font-mono); + color: var(--text-light); +} + +/* Dark mode adjustments for data tables */ +[data-bs-theme="dark"] .object-viewer-sql-results .table-dark th { + background-color: var(--background-alt); + color: var(--text-light); + border: none; + border-bottom: 2px solid var(--primary-dark); + position: sticky; + top: 0; + z-index: 10; +} + +[data-bs-theme="dark"] .object-viewer-sql-results .table th, +[data-bs-theme="dark"] .object-viewer-sql-results .table td { + border-left: 1px solid var(--border); + border-right: 1px solid var(--border); +} + +[data-bs-theme="dark"] .object-viewer-sql-results tbody tr:nth-child(odd) { + background-color: rgba(31, 41, 55, 0.5); +} + +[data-bs-theme="dark"] .object-viewer-sql-results tbody tr:hover { + background-color: rgba(59, 130, 246, 0.1); +} \ No newline at end of file diff --git a/webui/src/styles/objects/objects.css b/webui/src/styles/objects/objects.css new file mode 100644 index 00000000000..8fddde2f289 --- /dev/null +++ b/webui/src/styles/objects/objects.css @@ -0,0 +1,378 @@ +/* Tree Table */ +.tree-container { + margin-bottom: var(--spacing-xl); +} + +.tree-container .card { + border-radius: var(--radius-md); + overflow: hidden; +} + +.tree-container .card-header { + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--surface); + border-bottom: 1px solid var(--border); +} + +.tree-container .card-body { + padding: 0; +} + +.tree-entry-row, +.change-entry-row { + transition: background-color var(--transition-fast); +} + +.tree-entry-row:hover, +.change-entry-row:hover { + background-color: var(--border-light); +} + +.tree-path { + font-size: 0.9rem; +} + +.tree-path .octicon { + color: inherit; + margin-right: var(--spacing-xs); +} + +/* Make icons in diff rows match their text color */ +.diff-changed .tree-path .octicon { + color: inherit; +} + +.diff-added .tree-path .octicon { + color: inherit; +} + +.diff-removed .tree-path .octicon { + color: inherit; +} + +.diff-conflict .tree-path .octicon { + color: inherit; +} + +.prefix-expand-icon { + margin-left: 10px; + cursor: pointer; + color: var(--bs-nav-link-color); +} + +.tree-path-action { + cursor: pointer; + color: var(--bs-nav-link-color); +} + +/* Gear icon visibility and alignment */ +.change-entry-row-actions .dropdown-toggle, +.row-hover .btn-outline-danger { + visibility: hidden; + float: right; +} + +.change-entry-row:hover .change-entry-row-actions .dropdown-toggle, +tr:hover .row-hover .btn-outline-danger { + visibility: visible; +} + +/* Diff Styling */ +.diff-changed td { + background-color: var(--color-bg-changed); +} + +.diff-added td { + background-color: var(--color-bg-added); +} + +.diff-removed td { + background-color: var(--color-bg-removed); +} + +.diff-conflict td { + background-color: var(--color-bg-conflict); +} + +/* Improve link contrast on colored backgrounds */ +.diff-changed a, .diff-changed .btn-link { + color: #000000; + font-weight: 600; +} + +.diff-added a, .diff-added .btn-link { + color: #003300; + font-weight: 600; +} + +.diff-removed a, .diff-removed .btn-link { + color: #330000; + font-weight: 600; +} + +.diff-conflict a, .diff-conflict .btn-link { + color: #330033; + font-weight: 600; +} + +[data-bs-theme="dark"] tr.diff-changed > td , +[data-bs-theme="dark"] tr.diff-changed > td a, +[data-bs-theme="dark"] tr.diff-added > td , +[data-bs-theme="dark"] tr.diff-added > td a, +[data-bs-theme="dark"] tr.diff-removed > td, +[data-bs-theme="dark"] tr.diff-removed > td a, +[data-bs-theme="dark"] tr.diff-conflict > td , +[data-bs-theme="dark"] tr.diff-conflict > td a{ + color: var(--surface); +} + +/* Ensure hover states maintain contrast */ +.diff-changed a:hover, .diff-changed .btn-link:hover { + color: #333333; +} + +.diff-added a:hover, .diff-added .btn-link:hover { + color: #006600; +} + +.diff-removed a:hover, .diff-removed .btn-link:hover { + color: #660000; +} + +.diff-conflict a:hover, .diff-conflict .btn-link:hover { + color: #660066; +} + +[data-bs-theme="dark"] .diff-changed a:hover, +[data-bs-theme="dark"] .diff-changed .btn-link:hover { + color: #dddddd; +} + +[data-bs-theme="dark"] .diff-added a:hover, +[data-bs-theme="dark"] .diff-added .btn-link:hover { + color: #88ff88; +} + +[data-bs-theme="dark"] .diff-removed a:hover, +[data-bs-theme="dark"] .diff-removed .btn-link:hover { + color: #ff8888; +} + +[data-bs-theme="dark"] .diff-conflict a:hover, +[data-bs-theme="dark"] .diff-conflict .btn-link:hover { + color: #ff88ff; +} + +.color-fg-added { + color: var(--color-fg-added); + font-weight: 600; +} + +.color-fg-removed { + color: var(--color-fg-removed); + font-weight: 600; +} + +.color-fg-changed { + color: var(--color-fg-changed); + font-weight: 600; +} + +.color-fg-conflict { + color: var(--color-fg-conflict); + font-weight: 600; +} + +/* Object viewer styles */ +.object-viewer-buttons { + display: flex; + align-items: center; +} + +.object-viewer-buttons .btn { + margin-right: var(--spacing-md); +} + +.object-viewer-buttons .btn:last-child { + margin-right: 0; +} + +/* Make images in object viewer scale to fit their container */ +.object-viewer img, +[class*="object-viewer"] img, +[class*="fileViewer"] img, +.file-content-body img { + max-width: 100%; + height: auto; + object-fit: contain; +} + +.object-viewer-pdf object { + width: 100%; + height: 600px; +} + +/* Upload modal styles */ +.file-drop-zone { + border: 2px dashed var(--border); + border-radius: var(--radius); + padding: 2rem; + text-align: center; + background-color: var(--background-alt); + cursor: pointer; + transition: all var(--transition); +} + +.file-drop-zone:hover, +.file-drop-zone-focus { + border-color: var(--primary); + background-color: rgba(59, 130, 246, 0.05); +} + +/* Upload item styles */ +.upload-item { + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-light); +} + +.upload-item:last-child { + border-bottom: none; +} + +.upload-item-done { + background-color: rgba(16, 185, 129, 0.1); +} + +.upload-item-error { + background-color: rgba(239, 68, 68, 0.1); +} + +.upload-item .path { + font-family: var(--font-mono); + font-size: 0.85rem; +} + +.upload-item .size { + font-size: 0.85rem; + color: var(--text-light); +} + +/* Data Table Specific Styling (often used in object views like SQL results) */ +.object-viewer-sql-results { + margin-top: var(--spacing-md); + border-radius: var(--radius-md); + overflow: auto; + height: 70vh; /* Fixed height instead of max-height to ensure sticky headers work */ + box-shadow: var(--shadow-md); + position: relative; /* Needed for sticky positioning context */ +} + +.object-viewer-sql-results .table { + margin-bottom: 0; + border: 1px solid var(--border); +} + +.object-viewer-sql-results .table-dark th { + background-color: var(--surface); + color: var(--text); + border: none; + border-bottom: 2px solid var(--primary); + font-family: var(--font-mono); + font-size: 0.9rem; + padding: var(--spacing-md) var(--spacing-lg); + position: sticky; + top: 0; + z-index: 10; +} + +.object-viewer-sql-results .table { + border-collapse: collapse; +} + +.object-viewer-sql-results .table th, +.object-viewer-sql-results .table td { + border-left: 1px solid var(--border); + border-right: 1px solid var(--border); +} + +.object-viewer-sql-results .table th:first-child, +.object-viewer-sql-results .table td:first-child { + border-left: 1px solid var(--border); +} + +.object-viewer-sql-results .table th:last-child, +.object-viewer-sql-results .table td:last-child { + border-right: 1px solid var(--border); +} + +.object-viewer-sql-results .table-dark th small { + color: var(--text-light); + font-size: 0.75rem; + display: block; + margin-top: 0.25rem; + font-weight: normal; +} + +.object-viewer-sql-results tbody tr:nth-child(odd) { + background-color: var(--background-alt); +} + +.object-viewer-sql-results tbody tr:hover { + background-color: rgba(59, 130, 246, 0.05); +} + +.object-viewer-sql-results td { + font-size: 0.9rem; + padding: var(--spacing-sm) var(--spacing-md); + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.object-viewer-sql-results td.number-cell { + text-align: right; + font-family: var(--font-mono); +} + +.object-viewer-sql-results td.string-cell { + font-family: var(--font-sans); +} + +.object-viewer-sql-results td.date-cell { + font-family: var(--font-mono); + color: var(--text-light); +} + +/* Dark mode adjustments for data tables */ +[data-bs-theme="dark"] .object-viewer-sql-results .table-dark th { + background-color: var(--background-alt); + color: var(--text-light); + border: none; + border-bottom: 2px solid var(--primary-dark); + position: sticky; + top: 0; + z-index: 10; +} + +[data-bs-theme="dark"] .object-viewer-sql-results .table th, +[data-bs-theme="dark"] .object-viewer-sql-results .table td { + border-left: 1px solid var(--border); + border-right: 1px solid var(--border); +} + +[data-bs-theme="dark"] .object-viewer-sql-results tbody tr:nth-child(odd) { + background-color: rgba(31, 41, 55, 0.5); +} + +[data-bs-theme="dark"] .object-viewer-sql-results tbody tr:hover { + background-color: rgba(59, 130, 246, 0.1); +} + +/* Fix for object browser gear icon dropdown */ +.tree-container .dropdown-menu, +.change-entry-row .dropdown-menu { + position: fixed !important; /* Use fixed positioning to avoid container constraints */ + max-height: 80vh; /* Limit height to 80% of viewport height */ + overflow-y: auto; /* Add scrolling if needed */ +} \ No newline at end of file diff --git a/webui/src/styles/objects/tree.css b/webui/src/styles/objects/tree.css new file mode 100644 index 00000000000..3e6e3ad6612 --- /dev/null +++ b/webui/src/styles/objects/tree.css @@ -0,0 +1,86 @@ +/* Tree Table */ +.tree-container { + margin-bottom: var(--spacing-xl); +} + +.tree-container .card { + border-radius: var(--radius-md); + overflow: hidden; +} + +.tree-container .card-header { + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--surface); + border-bottom: 1px solid var(--border); +} + +.tree-container .card-body { + padding: 0; +} + +.tree-entry-row, +.change-entry-row { + transition: background-color var(--transition-fast); +} + +.tree-entry-row:hover, +.change-entry-row:hover { + background-color: var(--border-light); +} + +.tree-path { + font-size: 0.9rem; +} + +.tree-path .octicon { + color: inherit; + margin-right: var(--spacing-xs); +} + +/* Make icons in diff rows match their text color */ +.diff-changed .tree-path .octicon { + color: inherit; +} + +.diff-added .tree-path .octicon { + color: inherit; +} + +.diff-removed .tree-path .octicon { + color: inherit; +} + +.diff-conflict .tree-path .octicon { + color: inherit; +} + +.prefix-expand-icon { + margin-left: 10px; + cursor: pointer; + color: var(--bs-nav-link-color); +} + +.tree-path-action { + cursor: pointer; + color: var(--bs-nav-link-color); +} + +/* Gear icon visibility and alignment */ +.change-entry-row-actions .dropdown-toggle, +.row-hover .btn-outline-danger { + visibility: hidden; + float: right; +} + +.change-entry-row:hover .change-entry-row-actions .dropdown-toggle, +tr:hover .row-hover .btn-outline-danger { + visibility: visible; +} + +/* Fix for object browser gear icon dropdown */ +.tree-container .dropdown-menu, +.change-entry-row .dropdown-menu { + position: fixed !important; /* Use fixed positioning to avoid container constraints */ + max-height: 80vh; /* Limit height to 80% of viewport height */ + overflow-y: auto; /* Add scrolling if needed */ +} \ No newline at end of file diff --git a/webui/src/styles/objects/upload.css b/webui/src/styles/objects/upload.css new file mode 100644 index 00000000000..e0c6a13d454 --- /dev/null +++ b/webui/src/styles/objects/upload.css @@ -0,0 +1,44 @@ +/* Upload modal styles */ +.file-drop-zone { + border: 2px dashed var(--border); + border-radius: var(--radius); + padding: 2rem; + text-align: center; + background-color: var(--background-alt); + cursor: pointer; + transition: all var(--transition); +} + +.file-drop-zone:hover, +.file-drop-zone-focus { + border-color: var(--primary); + background-color: rgba(59, 130, 246, 0.05); +} + +/* Upload item styles */ +.upload-item { + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-light); +} + +.upload-item:last-child { + border-bottom: none; +} + +.upload-item-done { + background-color: rgba(16, 185, 129, 0.1); +} + +.upload-item-error { + background-color: rgba(239, 68, 68, 0.1); +} + +.upload-item .path { + font-family: var(--font-mono); + font-size: 0.85rem; +} + +.upload-item .size { + font-size: 0.85rem; + color: var(--text-light); +} \ No newline at end of file diff --git a/webui/src/styles/repositories/repositories.css b/webui/src/styles/repositories/repositories.css new file mode 100644 index 00000000000..47931280e77 --- /dev/null +++ b/webui/src/styles/repositories/repositories.css @@ -0,0 +1,224 @@ +/* Repository Item */ +.repository-item .card { + transition: transform var(--transition), box-shadow var(--transition); +} + +.repository-item .card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.repository-item h5 { + margin-bottom: var(--spacing-xs); +} + +.repository-item p { + color: var(--text-light); + margin-bottom: 0; +} + +.repository-item code { + color: var(--primary-dark); + background-color: var(--border-light); +} + +/* Enhanced Repository Card Styling */ +.repository-card { + border-radius: var(--radius-lg); + border: 1px solid var(--border); + box-shadow: var(--shadow); + transition: all var(--transition); + overflow: hidden; + padding: var(--spacing-sm); +} + +.repository-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-lg); + border-color: var(--primary-light); +} + +/* Repository Title Styling */ +.repository-title a { + font-weight: 600; + font-size: 1.2rem; + color: var(--text-dark); + transition: color var(--transition-fast); +} + +.repository-title a:hover { + color: var(--primary); + text-decoration: none; +} + +/* Repository Created Date Styling */ +.repository-created-date { + font-size: 0.8rem; + color: var(--text-light); + margin-top: 0.25rem; +} + +/* Repository Details Styling */ +.repository-details { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.detail-item { + display: flex; + flex-direction: column; + background-color: var(--background-alt); + border-radius: var(--radius); + padding: var(--spacing-sm); + transition: background-color var(--transition-fast); +} + +.detail-item:hover { + background-color: var(--border-light); +} + +.detail-label { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-light); + margin-bottom: var(--spacing-xs); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-value { + font-size: 0.95rem; + color: var(--text); +} + +.detail-value code { + color: var(--primary-dark); + background-color: rgba(59, 130, 246, 0.1); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-sm); + font-size: 0.9rem; + border: none; + word-break: break-all; +} + +/* Compact Repository Details Styling */ +.repository-details-compact { + margin-top: var(--spacing-sm); + border-top: 1px solid var(--border-light); + padding-top: var(--spacing-sm); +} + +.detail-row { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); +} + +.detail-item-compact { + display: flex; + flex-direction: column; + background-color: var(--background-alt); + border-radius: var(--radius-sm); + padding: var(--spacing-xs) var(--spacing-sm); + transition: background-color var(--transition-fast); + flex: 1; + min-width: 120px; +} + +.detail-item-compact:hover { + background-color: var(--border-light); +} + +.detail-label-compact { + font-size: 0.7rem; + font-weight: 600; + color: var(--text-light); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-value-compact { + font-size: 0.85rem; + color: var(--text); + margin-top: 0.1rem; +} + +.detail-value-compact code { + color: var(--primary-dark); + background-color: rgba(59, 130, 246, 0.1); + padding: 0.1rem 0.3rem; + border-radius: var(--radius-sm); + font-size: 0.8rem; + border: none; + word-break: break-all; + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + +.storage-namespace { + flex: 2; + min-width: 200px; +} + +/* Dark Mode Adjustments */ +[data-bs-theme="dark"] .repository-card { + background-color: var(--surface); + border-color: var(--border); +} + +[data-bs-theme="dark"] .repository-card:hover { + border-color: var(--primary); +} + +[data-bs-theme="dark"] .detail-item { + background-color: rgba(59, 130, 246, 0.05); +} + +[data-bs-theme="dark"] .detail-item:hover { + background-color: rgba(59, 130, 246, 0.1); +} + +[data-bs-theme="dark"] .detail-value code { + background-color: rgba(59, 130, 246, 0.15); + color: var(--primary-light); +} + +/* Dark mode fix for repo item code */ +[data-bs-theme="dark"] .repository-item code { + background-color: #374151; + color: #93c5fd; + border: 1px solid #4b5563; +} + +/* Commit List */ +.commit-list .list-group-item { + border-left: none; + border-right: none; + padding: var(--spacing-md) var(--spacing-lg); + transition: background-color var(--transition-fast); +} + +.commit-list .list-group-item:hover { + background-color: var(--border-light); +} + +.commit-list .list-group-item:nth-of-type(even) { + background-color: var(--background-alt); +} + +.commit-list .list-group-item:nth-of-type(even):hover { + background-color: var(--border-light); +} + +/* Commit Actions */ +.commit-actions .btn, +.branch-actions .btn, +.run-commit { + font-family: var(--font-mono); + font-size: 0.8rem; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); +} \ No newline at end of file From 90a0535fb32051449474606b8e78c6fc27153d63 Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Mon, 5 May 2025 20:50:38 -0400 Subject: [PATCH 2/8] overhaul upload modal --- .../src/lib/components/repository/changes.jsx | 22 +- .../pages/repositories/repository/objects.jsx | 496 +++++++++++++----- webui/src/styles/objects/upload.css | 129 ++++- 3 files changed, 505 insertions(+), 142 deletions(-) diff --git a/webui/src/lib/components/repository/changes.jsx b/webui/src/lib/components/repository/changes.jsx index 04d33f74646..67238ea85fe 100644 --- a/webui/src/lib/components/repository/changes.jsx +++ b/webui/src/lib/components/repository/changes.jsx @@ -165,7 +165,27 @@ export const TreeEntryPaginator = ({ path, setAfterUpdated, nextPage, depth=0, l ); }; - +/** + * A container component for entries that represent a diff between refs. This container is used by the compare, commit changes, + * and uncommitted changes views. + * + * @param results to be displayed in the changes tree container + * @param delimiter objects delimiter ('' or '/') + * @param uriNavigator to navigate in the page using the changes container + * @param leftDiffRefID commitID / branch + * @param rightDiffRefID commitID / branch + * @param repo Repository + * @param reference commitID / branch + * @param internalRefresh to be called when the page refreshes manually + * @param prefix for which changes are displayed + * @param getMore to be called when requesting more diff results for a prefix + * @param loading of API response state to get changes + * @param nextPage of API response state to get changes + * @param setAfterUpdated state of pagination of the item's children + * @param onNavigate to be called when navigating to a prefix + * @param onRevert to be called when an object/prefix is requested to be reverted + * @param changesTreeMessage + */ export const ChangesTreeContainer = ({results, delimiter, uriNavigator, leftDiffRefID, rightDiffRefID, repo, reference, internalRefresh, prefix, getMore, loading, nextPage, setAfterUpdated, onNavigate, onRevert, diff --git a/webui/src/pages/repositories/repository/objects.jsx b/webui/src/pages/repositories/repository/objects.jsx index c884884648e..fa823c09f39 100644 --- a/webui/src/pages/repositories/repository/objects.jsx +++ b/webui/src/pages/repositories/repository/objects.jsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import dayjs from "dayjs"; import { useOutletContext } from "react-router-dom"; -import {CheckboxIcon, UploadIcon, XIcon} from "@primer/octicons-react"; +import {CheckboxIcon, UploadIcon, XIcon, AlertIcon, PencilIcon} from "@primer/octicons-react"; import RefDropdown from "../../../lib/components/repository/refDropdown"; import { ActionGroup, @@ -41,9 +41,6 @@ import { useSearchParams } from "react-router-dom"; import { useStorageConfigs } from "../../../lib/hooks/storageConfig"; import { getRepoStorageConfig } from "./utils"; import {useDropzone} from "react-dropzone"; -import Container from "react-bootstrap/Container"; -import Row from "react-bootstrap/Row"; -import Col from "react-bootstrap/Col"; import pMap from "p-map"; const README_FILE_NAME = "README.md"; @@ -242,69 +239,149 @@ function extractChecksumFromResponse(parsedHeaders) { return null; } -const uploadFile = async (config, repo, reference, path, file, onProgress) => { - const fpath = destinationPath(path, file); +const uploadFile = async (config, repo, reference, destinationPath, file, onProgress) => { if (config.pre_sign_support_ui) { let additionalHeaders; if (config.blockstore_type === "azure") { additionalHeaders = { "x-ms-blob-type": "BlockBlob" } } - const getResp = await staging.get(repo.id, reference.id, fpath, config.pre_sign_support_ui); + const getResp = await staging.get(repo.id, reference.id, destinationPath, config.pre_sign_support_ui); try { const uploadResponse = await uploadWithProgress(getResp.presigned_url, file, 'PUT', onProgress, additionalHeaders); const parsedHeaders = parseRawHeaders(uploadResponse.rawHeaders); const checksum = extractChecksumFromResponse(parsedHeaders); - await staging.link(repo.id, reference.id, fpath, getResp, checksum, file.size, file.type); + await staging.link(repo.id, reference.id, destinationPath, getResp, checksum, file.size, file.type); } catch(error) { throw new Error(`Error uploading file- HTTP ${error.status}${error.response ? `: ${error.response}` : ''}`); } } else { - await objects.upload(repo.id, reference.id, fpath, file, onProgress); + await objects.upload(repo.id, reference.id, destinationPath, file, onProgress); } }; -const destinationPath = (path, file) => { - return `${path ? path : ""}${file.path.replace(/\\/g, '/').replace(/^\//, '')}`; +const joinPath = (basePath, filePath) => { + // 1. Normalize the file path first: remove leading slash + const normalizedFilePath = filePath.replace(/^\//, ''); + + // 2. Handle the base path + // If basePath is empty or just '/', the result is just the normalized file path + if (!basePath || basePath === '/') { + return normalizedFilePath; + } + + // 3. If basePath is non-empty, ensure it ends with '/' + const normalizedBasePath = basePath.endsWith('/') ? basePath : basePath + '/'; + + // 4. Combine + return normalizedBasePath + normalizedFilePath; +} + +const generateInitialDestination = (filePath, currentOverallPath) => { + const relativePathFromDrop = filePath.replace(/\\/g, '/'); + return joinPath(currentOverallPath, relativePathFromDrop); }; -const UploadCandidate = ({ repo, reference, path, file, state, onRemove = null }) => { - const fpath = destinationPath(path, file) - let uploadIndicator = null; +const UploadCandidate = ({ + file, + state, + destination, + onDestinationChange, + onRemove, + isUploading, + isEditing, + onEditToggle, +}) => { + let statusIndicator = null; + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + if (state && state.status === "uploading") { - uploadIndicator = + statusIndicator = ; } else if (state && state.status === "done") { - uploadIndicator = - } else if (!state && onRemove !== null) { - uploadIndicator = ( - { - e.preventDefault() - onRemove() - }}> + statusIndicator = ; + } else if (state && state.status === "error") { + statusIndicator = ; + } else if (onRemove !== null && !isUploading && state?.status !== 'uploading') { + statusIndicator = ( + ); } + + const handleBlur = () => { + onEditToggle(false); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter' || e.key === 'Escape') { + onEditToggle(false); + e.preventDefault(); // Prevent form submission on Enter + } + }; + return ( - - - - lakefs://{repo.id}/{reference.id}/{fpath} - - - - {humanSize(file.size)} - - - - - {uploadIndicator ? uploadIndicator : <>} - - - - +
+
+ {isEditing ? ( + onDestinationChange(e.target.value)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + disabled={isUploading} + placeholder="Destination path/name" + /> + ) : ( + <> + !isUploading && onEditToggle(true)} + > + {destination ||  } + + {!isUploading && state?.status !== 'uploading' && state?.status !== 'done' && state?.status !== 'error' && ( + + )} + + )} +
+
+ {humanSize(file.size)} +
+
+ {statusIndicator} +
+
) }; @@ -314,108 +391,221 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s error: null, done: false, }; - const [currentPath, setCurrentPath] = useState(path); + const [overallPath, setOverallPath] = useState(path || ""); const [uploadState, setUploadState] = useState(initialState); const [files, setFiles] = useState([]); const [fileStates, setFileStates] = useState({}); - const [abortController, setAbortController] = useState(null) + const [fileDestinations, setFileDestinations] = useState({}); + const [editingDestinations, setEditingDestinations] = useState({}); + const [manuallyEditedDestinations, setManuallyEditedDestinations] = useState({}); + const [abortController, setAbortController] = useState(null); + const onDrop = useCallback(acceptedFiles => { if (uploadState.inProgress) return; - setFiles([...acceptedFiles]) - }, [files, uploadState.inProgress]) + + const newFiles = acceptedFiles.filter(f => !files.some(existing => existing.path === f.path)); + if (newFiles.length === 0) return; + + const nextFiles = [...files, ...newFiles]; + const nextDestinations = { ...fileDestinations }; + const nextStates = { ...fileStates }; + const nextEditing = { ...editingDestinations }; + const nextManualEditFlags = { ...manuallyEditedDestinations }; + + newFiles.forEach(file => { + const initialDest = generateInitialDestination(file.path, overallPath); + nextDestinations[file.path] = initialDest; + nextStates[file.path] = { status: 'pending', percent: 0 }; + nextEditing[file.path] = false; + nextManualEditFlags[file.path] = false; + }); + + setFiles(nextFiles); + setFileDestinations(nextDestinations); + setFileStates(nextStates); + setEditingDestinations(nextEditing); + setManuallyEditedDestinations(nextManualEditFlags); + + }, [files, fileDestinations, fileStates, editingDestinations, manuallyEditedDestinations, overallPath, uploadState.inProgress]); const { getRootProps, getInputProps, isDragAccept } = useDropzone({ onDrop, disabled: uploadState.inProgress, - noClick: uploadState.inProgress, - noKeyboard: uploadState.inProgress }) + useEffect(() => { + setFileDestinations(currentDestinations => { + const nextDestinations = { ...currentDestinations }; + let changed = false; + files.forEach(file => { + if (!manuallyEditedDestinations[file.path]) { + const newDest = generateInitialDestination(file.path, overallPath); + if (nextDestinations[file.path] !== newDest) { + nextDestinations[file.path] = newDest; + changed = true; + } + } + }); + return changed ? nextDestinations : currentDestinations; + }); + }, [overallPath, files, manuallyEditedDestinations]); + + if (!reference || reference.type !== RefTypeBranch) return <>; const hide = () => { if (uploadState.inProgress) { if (abortController !== null) { - abortController.abort() + abortController.abort(); } else { - return + return; } } setUploadState(initialState); - setFileStates({}); setFiles([]); - setCurrentPath(path); - setAbortController(null) + setFileStates({}); + setFileDestinations({}); + setEditingDestinations({}); + setManuallyEditedDestinations({}); + setOverallPath(path || ""); + setAbortController(null); onHide(); }; useEffect(() => { - setCurrentPath(path) + setOverallPath(path || "") }, [path]) const upload = async () => { - if (files.length < 1) { + if (files.length < 1 || uploadState.inProgress) { return } - const abortController = new AbortController() - setAbortController(abortController) + setEditingDestinations({}); + + const controller = new AbortController(); + setAbortController(controller); + setUploadState({ ...initialState, inProgress: true }); const mapper = async (file) => { + const currentDestination = fileDestinations[file.path]; + if (!currentDestination) { + console.error(`No destination path found for file: ${file.path}`); + setFileStates(next => ({ ...next, [file.path]: { status: 'error', percent: 0 } })); + throw new Error(`Missing destination for ${file.path}`); + } try { - setFileStates(next => ( {...next, [file.path]: {status: 'uploading', percent: 0}})) - await uploadFile(config, repo, reference, currentPath, file, progress => { - setFileStates(next => ( {...next, [file.path]: {status: 'uploading', percent: progress}})) - }) + setFileStates(next => ({ ...next, [file.path]: { status: 'uploading', percent: 0 } })); + + const handleProgress = (progress) => { + if (controller.signal.aborted) return; + setFileStates(next => { + if (next[file.path]?.status === 'uploading') { + return { ...next, [file.path]: { status: 'uploading', percent: progress } }; + } + return next; + }); + }; + + await uploadFile(config, repo, reference, currentDestination, file, handleProgress); + + if (controller.signal.aborted) return; + setFileStates(next => ({ ...next, [file.path]: { status: 'done', percent: 100 } })); + } catch (error) { - setFileStates(next => ( {...next, [file.path]: {status: 'error'}})) - setUploadState({ ...initialState, error }); + if (controller.signal.aborted) return; + console.error("Upload error for:", file.path, error); + setFileStates(next => ({ ...next, [file.path]: { status: 'error', percent: 0 } })); + if (!(error instanceof DOMException && error.name === 'AbortError') && !controller.signal.aborted) { + setUploadState(prev => ({ ...prev, error: error })); + } throw error; } - setFileStates(next => ( {...next, [file.path]: {status: 'done'}})) } - setUploadState({...initialState, inProgress: true }); try { await pMap(files, mapper, { concurrency: MAX_PARALLEL_UPLOADS, - signal: abortController.signal + signal: controller.signal, + stopOnError: true }); - onDone(); - hide(); - } catch (error) { - if (error instanceof DOMException) { - // abort! - onDone(); - hide(); - } else { - setUploadState({ ...initialState, error }); + if (!controller.signal.aborted) { + setUploadState(prev => ({ ...prev, inProgress: false, done: true, error: null })); + onDone(); + hide(); } + } catch (error) { + if (!(error instanceof DOMException && error.name === 'AbortError') && !controller.signal.aborted) { + console.error("pMap upload error:", error); + setUploadState(prev => ({...prev, inProgress: false, error: prev.error || error })); + } else { + console.log("Upload process aborted."); + setUploadState(prev => ({...prev, inProgress: false})); + } + } finally { + setUploadState(prev => ({...prev, inProgress: false})); + setAbortController(null); } - - }; - const changeCurrentPath = useCallback(e => { - setCurrentPath(e.target.value) - }, [setCurrentPath]) - - const onRemoveCandidate = useCallback(file => { - return () => setFiles(current => current.filter(f => f !== file)) - }, [setFiles]) + const handleOverallPathChange = useCallback(e => { + setOverallPath(e.target.value) + }, []) + + const handleIndividualDestinationChange = useCallback((originalPath, newDestination) => { + setFileDestinations(prev => ({ ...prev, [originalPath]: newDestination })); + setManuallyEditedDestinations(prev => ({ ...prev, [originalPath]: true })); + }, []); + + const handleRemoveFile = useCallback(originalPath => { + setFiles(prev => prev.filter(f => f.path !== originalPath)); + setFileDestinations(prev => { + const next = { ...prev }; + delete next[originalPath]; + return next; + }); + setFileStates(prev => { + const next = { ...prev }; + delete next[originalPath]; + return next; + }); + setEditingDestinations(prev => { + const next = { ...prev }; + delete next[originalPath]; + return next; + }); + setManuallyEditedDestinations(prev => { + const next = { ...prev }; + delete next[originalPath]; + return next; + }); + }, []); + + const handleEditToggle = useCallback((originalPath, editMode) => { + setEditingDestinations(prev => ({ ...prev, [originalPath]: editMode })); + }, []); + + const totalSize = files.reduce((a, f) => a + f.size, 0); + const canUpload = files.length > 0 && !uploadState.inProgress; + + const totalProgress = files.reduce((sum, file) => sum + (fileStates[file.path]?.percent || 0), 0); + const averageProgress = files.length > 0 ? Math.round(totalProgress / files.length) : 0; + const uploadingCount = files.filter(f => fileStates[f.path]?.status === 'uploading').length; return ( <> - - - Upload Object + + + + + Upload Objects to Branch '{reference.id}' +
{ - if (uploadState.inProgress) return; e.preventDefault(); - upload(); + if (canUpload) upload(); }} > {config?.warnings && ( @@ -424,60 +614,98 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s )} - - Path - + +
+ +
+ +
+ {isDragAccept ? "Drop files here" : "Drag & drop files or folders here"} +
+
or click to browse
+
+
- -
- -
- {uploadState.inProgress ? "Upload in progress..." : "Drag 'n' drop files or folders here (or click to select)"} -
-
- -
+ )} + + + )} - {(uploadState.error) ? () : (<>)} -
- - - - -
+ {(uploadState.error && !(uploadState.error instanceof DOMException && uploadState.error.name === 'AbortError')) && + setUploadState(prev => ({...prev, error: null}))}/> + } + + + + + +
- - + Upload Object + + ); }; diff --git a/webui/src/styles/objects/upload.css b/webui/src/styles/objects/upload.css index e0c6a13d454..a9cdbb124a6 100644 --- a/webui/src/styles/objects/upload.css +++ b/webui/src/styles/objects/upload.css @@ -2,11 +2,16 @@ .file-drop-zone { border: 2px dashed var(--border); border-radius: var(--radius); - padding: 2rem; + padding: 3rem 2rem; text-align: center; background-color: var(--background-alt); cursor: pointer; transition: all var(--transition); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; } .file-drop-zone:hover, @@ -15,10 +20,53 @@ background-color: rgba(59, 130, 246, 0.05); } -/* Upload item styles */ +.file-drop-zone-icon { + font-size: 2.5rem; + color: var(--text-light); +} + +.file-drop-zone-text { + font-weight: 500; +} + +.file-drop-zone-hint { + font-size: 0.85rem; + color: var(--text-light); +} + +/* Upload items container */ +.upload-items-container { + margin-top: 1.5rem; +} + +.upload-items-header { + display: grid; + /* Columns: Destination, Size, Status */ + grid-template-columns: 1fr 120px 80px; + gap: 1rem; + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border); + font-weight: 500; + font-size: 0.85rem; + color: var(--text-light); + text-align: left; +} + +.upload-items-list { + max-height: 40vh; /* Adjust height */ + overflow-y: auto; + border: 1px solid var(--border-light); + border-radius: var(--radius); +} + +/* Individual Upload item styles */ .upload-item { - padding: 0.5rem 0; + display: grid; + grid-template-columns: 1fr 120px 80px; + gap: 1rem; + padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-light); + align-items: center; } .upload-item:last-child { @@ -26,19 +74,86 @@ } .upload-item-done { - background-color: rgba(16, 185, 129, 0.1); + /* Optional: subtle background for completed items */ + /* background-color: rgba(16, 185, 129, 0.05); */ } .upload-item-error { background-color: rgba(239, 68, 68, 0.1); } -.upload-item .path { - font-family: var(--font-mono); +.upload-item .form-control { + font-size: 0.85rem; + padding: 0.375rem 0.75rem; +} + +/* Destination File column styling */ +.file-destination-column { + display: flex; + align-items: center; /* Vertically align text/input and icon */ + gap: 0.5rem; /* Space between text/input and icon */ + overflow: hidden; +} + +.file-destination-display { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; font-size: 0.85rem; + line-height: 1.3; + flex-grow: 1; /* Allow text to take available space */ + cursor: pointer; /* Indicate clickability */ + padding: 0.375rem 0; /* Align vertically with input */ } -.upload-item .size { +.upload-item .file-size { font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-light); + text-align: left; +} + +.upload-item .upload-progress { + height: 8px; + margin-bottom: 0; + width: 100%; /* Ensure progress takes full width */ +} + +.upload-item .upload-status { + display: flex; + /* Removed justify-content: center */ + justify-content: flex-start; /* Align status icons/progress left */ + align-items: center; + padding-left: 0.5rem; /* Add some padding */ +} + +.upload-item .upload-status .text-success, +.upload-item .upload-status .text-danger { + font-size: 1.1rem; +} + +.edit-destination-button { + /* Removed absolute positioning */ + background: none; + border: none; color: var(--text-light); + padding: 0; + margin: 0; + line-height: 1; + opacity: 0.6; /* Slightly visible */ + transition: opacity 0.2s ease, color 0.2s ease; + cursor: pointer; + flex-shrink: 0; /* Prevent icon from shrinking */ +} + +.edit-destination-button:hover { + color: var(--primary); + opacity: 1; +} + +/* Ensure input still takes full width within its flex container */ +.file-destination-column > .form-control { + flex-grow: 1; } \ No newline at end of file From d9bdd974e721580b6325595e7ec866271d2c5084 Mon Sep 17 00:00:00 2001 From: Barak Amar Date: Sun, 25 May 2025 12:45:28 +0300 Subject: [PATCH 3/8] Fix Playwright webui test code (#9098) --- webui/test/e2e/common/quickstart.spec.ts | 6 ++-- webui/test/e2e/common/setup.spec.ts | 2 +- webui/test/e2e/common/test-upload.txt | 1 + webui/test/e2e/common/uploadFile.spec.ts | 17 +++++---- .../test/e2e/common/viewParquetObject.spec.ts | 2 +- webui/test/e2e/poms/repositoriesPage.ts | 10 +++--- webui/test/e2e/poms/repositoryPage.ts | 35 ++++++------------- 7 files changed, 29 insertions(+), 44 deletions(-) create mode 100644 webui/test/e2e/common/test-upload.txt diff --git a/webui/test/e2e/common/quickstart.spec.ts b/webui/test/e2e/common/quickstart.spec.ts index 798684019a1..f8244660e19 100644 --- a/webui/test/e2e/common/quickstart.spec.ts +++ b/webui/test/e2e/common/quickstart.spec.ts @@ -37,7 +37,7 @@ test.describe("Quickstart", () => { const repositoryPage = new RepositoryPage(page); await repositoryPage.clickObject(PARQUET_OBJECT_NAME); - await expect(page.getByText("Loading...")).not.toBeVisible(); + await expect(page.getByRole("button", { name: "Execute" })).toBeVisible(); const objectViewerPage = new ObjectViewerPage(page); await objectViewerPage.enterQuery(SELECT_QUERY); @@ -55,7 +55,7 @@ test.describe("Quickstart", () => { await repositoryPage.gotoObjectsTab(); await repositoryPage.clickObject(PARQUET_OBJECT_NAME); - await expect(page.getByText("Loading...")).not.toBeVisible(); + await expect(page.getByRole("button", { name: "Execute" })).toBeVisible(); const objectViewerPage = new ObjectViewerPage(page); await objectViewerPage.enterQuery(CREATE_TABLE_QUERY); @@ -96,7 +96,7 @@ test.describe("Quickstart", () => { await repositoriesPage.goto(); await repositoriesPage.goToRepository(QUICKSTART_REPO_NAME); await repositoryPage.clickObject(PARQUET_OBJECT_NAME); - await expect(page.getByText("Loading...")).not.toBeVisible(); + await expect(page.getByRole("button", { name: "Execute" })).toBeVisible(); const objectViewerPage = new ObjectViewerPage(page); await objectViewerPage.enterQuery(SELECT_QUERY); await objectViewerPage.clickExecuteButton(); diff --git a/webui/test/e2e/common/setup.spec.ts b/webui/test/e2e/common/setup.spec.ts index 026932af3c1..9ece30ca51b 100644 --- a/webui/test/e2e/common/setup.spec.ts +++ b/webui/test/e2e/common/setup.spec.ts @@ -13,7 +13,7 @@ test.describe("Setup Page", () => { await page.waitForURL(/.*\/setup/); }); - test("username has a defualt value of 'admin'", async ({ page }) => { + test("username has a default value of 'admin'", async ({ page }) => { const setupPage = new SetupPage(page); await setupPage.goto(); const usernameInput = setupPage.usernameInputLocator; diff --git a/webui/test/e2e/common/test-upload.txt b/webui/test/e2e/common/test-upload.txt new file mode 100644 index 00000000000..d795da655a7 --- /dev/null +++ b/webui/test/e2e/common/test-upload.txt @@ -0,0 +1 @@ +This is a test file for Playwright upload. \ No newline at end of file diff --git a/webui/test/e2e/common/uploadFile.spec.ts b/webui/test/e2e/common/uploadFile.spec.ts index 06049cd7b10..beb77c381a8 100644 --- a/webui/test/e2e/common/uploadFile.spec.ts +++ b/webui/test/e2e/common/uploadFile.spec.ts @@ -29,18 +29,17 @@ test.describe("Upload File", () => { await repositoryPage.uploadObject(filePath); // upload file, check path - await expect(page.getByRole('complementary')).toContainText(`lakefs://${TEST_REPO_NAME}/${TEST_BRANCH}/${FILE_NAME}`); - await page.getByRole('button', { name: 'Upload', exact: true }).click(); - await expect(page.getByRole('rowgroup')).toContainText(FILE_NAME); + await expect(page.getByText(FILE_NAME)).toBeVisible(); + await page.getByRole('button', { name: 'Upload 1 File' }).click(); + await expect(page.getByRole('cell', {name: FILE_NAME})).toBeVisible(); // upload again with prefix, check path await repositoryPage.uploadObject(filePath); - await page.locator('#path').click(); - await page.locator('#path').fill(PREFIX); - await expect(page.getByRole('complementary')).toContainText(`lakefs://${TEST_REPO_NAME}/${TEST_BRANCH}/${PREFIX}${FILE_NAME}`); - await page.getByRole('button', { name: 'Upload', exact: true }).click(); - await page.getByRole('link', { name: PREFIX }).click(); - await expect(page.getByRole('row')).toContainText(FILE_NAME); + await page.getByRole('textbox', { name: 'Common Destination Directory' }).fill(PREFIX); + await expect(page.getByText(PREFIX+FILE_NAME)).toBeVisible(); + await page.getByRole('button', { name: 'Upload 1 File' }).click(); + await page.getByRole('cell', { name: PREFIX }).click(); + await expect(page.getByRole('cell', { name: FILE_NAME })).toBeVisible(); }); }) diff --git a/webui/test/e2e/common/viewParquetObject.spec.ts b/webui/test/e2e/common/viewParquetObject.spec.ts index 34b72882959..96c1bd87bee 100644 --- a/webui/test/e2e/common/viewParquetObject.spec.ts +++ b/webui/test/e2e/common/viewParquetObject.spec.ts @@ -22,7 +22,7 @@ test.describe("Object Viewer - Parquet File", () => { const repositoryPage = new RepositoryPage(page); await repositoryPage.clickObject(PARQUET_OBJECT_NAME); - await expect(page.getByText("Loading...")).not.toBeVisible(); + await expect(page.getByRole("button", { name: "Execute" })).toBeVisible(); }); test("view parquet object w/ logout and login", async ({page}) => { diff --git a/webui/test/e2e/poms/repositoriesPage.ts b/webui/test/e2e/poms/repositoriesPage.ts index 209af5905d3..7035c3e12e7 100644 --- a/webui/test/e2e/poms/repositoriesPage.ts +++ b/webui/test/e2e/poms/repositoriesPage.ts @@ -1,7 +1,7 @@ import { Locator, Page, expect } from "@playwright/test"; const SAMPLE_REPO_README_TITLE = "Welcome to the Lake!"; -const REGULAR_REPO_README_TITLE = "To get started with this repository:"; +const REGULAR_REPO_README_TITLE = "Your repository is ready!"; export class RepositoriesPage { private page: Page; @@ -16,9 +16,9 @@ export class RepositoriesPage { this.page = page; this.noRepositoriesTitleLocator = this.page.getByText("Welcome to LakeFS!"); this.readOnlyIndicatorLocator = this.page.locator("text=Read-only"); - this.uploadButtonLocator = this.page.locator("text=Upload Object"); + this.uploadButtonLocator = this.page.locator("text=Upload Object").first(); this.createRepositoryButtonLocator = this.page.getByRole("button", { name: "Create Repository" }); - this.searchInputLocator = this.page.getByPlaceholder("Find a repository..."); + this.searchInputLocator = this.page.getByPlaceholder("Search repositories..."); } async goto(): Promise { @@ -36,9 +36,9 @@ export class RepositoriesPage { async createRepository(repoName: string, includeSampleData: boolean): Promise { await this.createRepositoryButtonLocator.click(); - await this.page.getByLabel("Repository ID").fill(repoName); + await this.page.getByRole('textbox', { name: 'Repository ID' }).fill(repoName); if (includeSampleData) { - await this.page.getByLabel("Add sample data, hooks, and configuration").check(); + await this.page.getByRole('checkbox', { name: 'Add sample data, hooks' }).check(); } await this.page.getByRole("dialog").getByRole("button", { name: "Create Repository", exact: true }).click(); if (includeSampleData) { diff --git a/webui/test/e2e/poms/repositoryPage.ts b/webui/test/e2e/poms/repositoryPage.ts index 21965998ab9..e9a393fa15f 100644 --- a/webui/test/e2e/poms/repositoryPage.ts +++ b/webui/test/e2e/poms/repositoryPage.ts @@ -44,27 +44,12 @@ export class RepositoryPage { // file manipulation operations async deleteFirstObjectInDirectory(dirName: string): Promise { - await this.page.getByRole("link", {name: dirName}).click(); - - const getFirstObjectRow = (page: Page) => page - .locator("table.table") - .locator("tbody") - .locator("tr") - .first(); - - await getFirstObjectRow(this.page) - .locator("div.dropdown") - .hover(); - await getFirstObjectRow(this.page) - .locator("div.dropdown") - .locator("button") - .click(); - await this.page - .locator("div.dropdown") - .locator(".dropdown-item") - .last() - .click(); - await this.page.getByRole("button", {name: "Yes"}).click(); + await this.page.getByRole("link", { name: dirName }).click(); + const firstRow = this.page.locator('table tbody tr').first(); + await firstRow.hover(); + await firstRow.locator('button').last().click(); + await this.page.getByRole('button', { name: 'Delete' }).click(); + await this.page.getByRole("button", { name: "Yes" }).click(); } // uncommitted changes operations @@ -132,9 +117,9 @@ export class RepositoryPage { } async uploadObject(filePath: string): Promise { - await this.page.getByRole("button", { name: "Upload Object" }).click(); - await this.page.getByText("Drag 'n' drop files or").click(); - const fileInput = await this.page.locator('input[type="file"]'); - await fileInput.setInputFiles(filePath); + await this.page.getByRole("button", { name: "Upload Object" }).click(); + await this.page.getByText("Drag & drop files or folders here").click(); + const fileInput = await this.page.locator('input[type="file"]'); + await fileInput.setInputFiles(filePath); } } From f5b6deed3f74d0303336eae15c4f67286ab1efa5 Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Mon, 26 May 2025 18:56:09 -0400 Subject: [PATCH 4/8] fixed review comments --- webui/src/lib/components/repository/tree.jsx | 11 +-- webui/src/pages/repositories/index.jsx | 22 +++--- .../pages/repositories/repository/objects.jsx | 72 ++++++++++++------- .../src/styles/repositories/repositories.css | 8 +++ 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/webui/src/lib/components/repository/tree.jsx b/webui/src/lib/components/repository/tree.jsx index 4e969d8ad45..a42440c841d 100644 --- a/webui/src/lib/components/repository/tree.jsx +++ b/webui/src/lib/components/repository/tree.jsx @@ -37,6 +37,7 @@ import { useAPI } from "../../hooks/api"; import noop from "lodash/noop"; import {CommitInfoCard} from "./commits"; + export const humanSize = (bytes) => { if (!bytes) return "0.0 B"; const e = Math.floor(Math.log(bytes) / Math.log(1024)); @@ -728,7 +729,7 @@ export const URINavigator = ({ ); }; -const GetStarted = ({ config, onUpload, onImport }) => { +const GetStarted = ({ config, onUpload, onImport, readOnly = false }) => { const importDisabled = !config.config.import_support; return ( @@ -742,9 +743,9 @@ const GetStarted = ({ config, onUpload, onImport }) => {

Your repository is ready!

-
Let's add some data to get started
+ {!readOnly &&
Let's add some data to get started
} - + {!readOnly &&
@@ -800,7 +801,7 @@ const GetStarted = ({ config, onUpload, onImport }) => { - + } ); }; @@ -823,7 +824,7 @@ export const Tree = ({ if (results.length === 0 && path === "" && reference.type === RefTypeBranch) { // empty state! body = ( - + ); } else { body = ( diff --git a/webui/src/pages/repositories/index.jsx b/webui/src/pages/repositories/index.jsx index 2f832a648d8..a70f658d98a 100644 --- a/webui/src/pages/repositories/index.jsx +++ b/webui/src/pages/repositories/index.jsx @@ -40,9 +40,9 @@ const CreateRepositoryButton = ({variant = "success", enabled = false, onClick}) ); } -const GettingStartedCreateRepoButton = ({text, variant = "success", enabled = false, onClick, creatingRepo, style = {}}) => { +const GettingStartedCreateRepoButton = ({text, variant = "success", enabled = false, onClick, creatingRepo, className = ""}) => { return ( - @@ -57,6 +57,13 @@ const CreateRepositoryModal = ({show, error, onSubmit, onCancel, inProgress}) => const {response: storageConfigs, error: err, loading} = useAPI(() => config.getStorageConfigs()); + const buttonContent = inProgress ? ( + <> + + Creating... + + ) : 'Create Repository'; + const showError = (error) ? error : err; if (loading) { return ( @@ -97,14 +104,7 @@ const CreateRepositoryModal = ({show, error, onSubmit, onCancel, inProgress}) => className="ms-2" disabled={!formValid || inProgress} > - {inProgress ? ( - <> - - Creating... - - ) : ( - 'Create Repository' - )} + {buttonContent} @@ -144,7 +144,7 @@ const GetStarted = ({allowSampleRepoCreation, onCreateSampleRepo, onCreateEmptyR
Already working with lakeFS? { + return (error instanceof DOMException && error.name === 'AbortError') || controller.signal.aborted; +}; + const ImportButton = ({ variant = "success", onClick, config }) => { const tip = config.import_support ? "Import data from a remote source" @@ -385,6 +389,22 @@ const UploadCandidate = ({ ) }; +const UploadButtonText = ({ inProgress, uploadingCount, filesLength, averageProgress }) => { + if (inProgress) { + return ( + <> + + Uploading {uploadingCount > 0 ? `${uploadingCount} / ${filesLength}` : averageProgress + '%'}... + + ); + } + return ( + <> + Upload {filesLength || ''} File{filesLength !== 1 ? 's' : ''} + + ); +}; + const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, show = false, disabled = false}) => { const initialState = { inProgress: false, @@ -516,7 +536,7 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s if (controller.signal.aborted) return; console.error("Upload error for:", file.path, error); setFileStates(next => ({ ...next, [file.path]: { status: 'error', percent: 0 } })); - if (!(error instanceof DOMException && error.name === 'AbortError') && !controller.signal.aborted) { + if (!isAbortedError(error, controller)) { setUploadState(prev => ({ ...prev, error: error })); } throw error; @@ -535,7 +555,7 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s hide(); } } catch (error) { - if (!(error instanceof DOMException && error.name === 'AbortError') && !controller.signal.aborted) { + if (!isAbortedError(error, controller)) { console.error("pMap upload error:", error); setUploadState(prev => ({...prev, inProgress: false, error: prev.error || error })); } else { @@ -653,24 +673,28 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s
- {files.map(file => - handleIndividualDestinationChange(file.path, newDest)} - onRemove={() => handleRemoveFile(file.path)} - isUploading={uploadState.inProgress || fileStates[file.path]?.status === 'uploading'} - isEditing={editingDestinations[file.path] || false} - onEditToggle={(editMode) => handleEditToggle(file.path, editMode)} - /> - )} + {files.map(file => { + const fileState = fileStates[file.path]; + const isUploading = uploadState.inProgress || fileState?.status === 'uploading'; + return ( + handleIndividualDestinationChange(file.path, newDest)} + onRemove={() => handleRemoveFile(file.path)} + isUploading={isUploading} + isEditing={editingDestinations[file.path] || false} + onEditToggle={(editMode) => handleEditToggle(file.path, editMode)} + /> + ); + })}
)} - {(uploadState.error && !(uploadState.error instanceof DOMException && uploadState.error.name === 'AbortError')) && + {(uploadState.error && !isAbortedError(uploadState.error, abortController)) && setUploadState(prev => ({...prev, error: null}))}/> } @@ -683,16 +707,12 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s disabled={!canUpload} onClick={upload} > - {uploadState.inProgress ? ( - <> - - Uploading {uploadingCount > 0 ? `${uploadingCount} / ${files.length}` : averageProgress + '%'}... - - ) : ( - <> - Upload {files.length || ''} File{files.length !== 1 ? 's' : ''} - - )} + diff --git a/webui/src/styles/repositories/repositories.css b/webui/src/styles/repositories/repositories.css index 47931280e77..af6fb51fb38 100644 --- a/webui/src/styles/repositories/repositories.css +++ b/webui/src/styles/repositories/repositories.css @@ -221,4 +221,12 @@ font-size: 0.8rem; padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); +} + +/* Inline Link Button */ +.inline-link-button { + padding: 0; + width: auto; + margin-left: 8px; + display: inline-block; } \ No newline at end of file From 284e45a3dde9e6d14c368d5d5072d7c027ac104b Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Mon, 26 May 2025 19:04:51 -0400 Subject: [PATCH 5/8] removed comments, fixed upload bug --- webui/src/pages/repositories/index.jsx | 2 -- webui/src/pages/repositories/repository/changes.jsx | 2 +- webui/src/pages/repositories/repository/objects.jsx | 10 +++++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/webui/src/pages/repositories/index.jsx b/webui/src/pages/repositories/index.jsx index a70f658d98a..60a9523b57c 100644 --- a/webui/src/pages/repositories/index.jsx +++ b/webui/src/pages/repositories/index.jsx @@ -186,7 +186,6 @@ const RepositoryList = ({ onPaginate, search, after, refresh, allowSampleRepoCre
- {/* Repository Header with Icon, Name, Badge and Creation Date */}
@@ -213,7 +212,6 @@ const RepositoryList = ({ onPaginate, search, after, refresh, allowSampleRepoCre
- {/* Repository Details in Horizontal Layout */}
diff --git a/webui/src/pages/repositories/repository/changes.jsx b/webui/src/pages/repositories/repository/changes.jsx index 216649a5096..6f7612e73f6 100644 --- a/webui/src/pages/repositories/repository/changes.jsx +++ b/webui/src/pages/repositories/repository/changes.jsx @@ -45,7 +45,7 @@ export const EmptyChangesState = ({ repo, reference }) => { href={{ pathname: "/repositories/:repoId/objects", params: { repoId: repo.id }, - query: { ref: reference.id } + query: { ref: reference.id, upload: true } }} className="btn btn-primary" > diff --git a/webui/src/pages/repositories/repository/objects.jsx b/webui/src/pages/repositories/repository/objects.jsx index ebc1ba0426d..0a91bc9239b 100644 --- a/webui/src/pages/repositories/repository/objects.jsx +++ b/webui/src/pages/repositories/repository/objects.jsx @@ -883,7 +883,7 @@ const NoGCRulesWarning = ({ repoId }) => { const ObjectsBrowser = ({ config }) => { const router = useRouter(); - const { path, after, importDialog } = router.query; + const { path, after, importDialog, upload } = router.query; const [searchParams, setSearchParams] = useSearchParams(); const { repo, reference, loading, error } = useRefs(); const [showUpload, setShowUpload] = useState(false); @@ -904,6 +904,14 @@ const ObjectsBrowser = ({ config }) => { } }, [router.route, importDialog, searchParams, setSearchParams]); + useEffect(() => { + if (upload) { + setShowUpload(true); + searchParams.delete("upload"); + setSearchParams(searchParams); + } + }, [router.route, upload, searchParams, setSearchParams]); + if (loading) return ; if (error) return ; From 94f809aaf14a447f0c55133b6fb79281259ddd59 Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Tue, 27 May 2025 18:07:58 -0400 Subject: [PATCH 6/8] review comments --- webui/src/lib/components/controls.jsx | 46 ++++----- .../src/lib/components/repository/changes.jsx | 94 ++++++++++--------- .../lib/components/repository/dataTable.jsx | 2 + .../repository/settings/branches.jsx | 45 +++++---- .../repository/settings/retention.jsx | 16 ++-- webui/src/styles/components/ui-components.css | 10 ++ 6 files changed, 114 insertions(+), 99 deletions(-) diff --git a/webui/src/lib/components/controls.jsx b/webui/src/lib/components/controls.jsx index e4f4b480761..300c05d262b 100644 --- a/webui/src/lib/components/controls.jsx +++ b/webui/src/lib/components/controls.jsx @@ -63,14 +63,21 @@ export const DebouncedFormControl = React.forwardRef((props, ref) => { }); DebouncedFormControl.displayName = "DebouncedFormControl"; + +export const Spinner = () => { + return ( +
+
+ Loading... +
+
+ ); +}; + export const Loading = ({message = "Loading..."}) => { return (
-
-
- Loading... -
-
+
{message}
@@ -93,34 +100,21 @@ export const AlertError = ({error, onDismiss = null, className = null}) => { const alertClassName = `${className} text-wrap text-break shadow-sm`.trim(); - if (onDismiss !== null) { - return ( - -
-
- -
-
{content}
-
-
- ); - } - return ( - -
+ +
{content}
- ); + ); }; export const FormattedDate = ({ dateValue, format = "MM/DD/YYYY HH:mm:ss" }) => { diff --git a/webui/src/lib/components/repository/changes.jsx b/webui/src/lib/components/repository/changes.jsx index 67238ea85fe..5eddbfbfb71 100644 --- a/webui/src/lib/components/repository/changes.jsx +++ b/webui/src/lib/components/repository/changes.jsx @@ -196,56 +196,58 @@ export const ChangesTreeContainer = ({results, delimiter, uriNavigator, if (results.length === 0) { if (emptyStateComponent) { return emptyStateComponent; - } else { - return
+ } + return ( +
{noChangesText}
- } - } else { - return
+ ); + } + return ( +
{changesTreeMessage &&
{changesTreeMessage}
} - - - {(delimiter !== "") && uriNavigator} -
- - - - - - - - -
-
+ + + {(delimiter !== "") && uriNavigator} +
+ + + + + + + + +
+
- -
- {field.name} -
- {field.type.toString()} +
+ {field.name} + {field.type.toString()} +
{value}{value}{dayjs(value).format()}{dayjs(value).format()}{value.toLocaleString("en-US")}{value.toLocaleString("en-US")}{"" + value}
- - {results.map(entry => { - return ( - ); - })} - {!!nextPage && - } - -
- - -
- } + + + + {results.map(entry => { + return ( + ); + })} + {!!nextPage && + } + +
+
+ +
+ ); } export const defaultGetMoreChanges = (repo, leftRefId, rightRefId, delimiter) => (afterUpdated, path, useDelimiter= true, amount = -1) => { diff --git a/webui/src/lib/components/repository/dataTable.jsx b/webui/src/lib/components/repository/dataTable.jsx index 5cfc482fd3d..6f10dc47afe 100644 --- a/webui/src/lib/components/repository/dataTable.jsx +++ b/webui/src/lib/components/repository/dataTable.jsx @@ -109,7 +109,9 @@ DataTable.propTypes = { Header: PropTypes.string.isRequired, accessor: PropTypes.string, Cell: PropTypes.func, + // className applies to the header cell (th element) className: PropTypes.string, + // cellClassName applies to all data cells (td elements) in this column cellClassName: PropTypes.string, description: PropTypes.string, }) diff --git a/webui/src/pages/repositories/repository/settings/branches.jsx b/webui/src/pages/repositories/repository/settings/branches.jsx index fcfdc83cc37..dab25dd9ea4 100644 --- a/webui/src/pages/repositories/repository/settings/branches.jsx +++ b/webui/src/pages/repositories/repository/settings/branches.jsx @@ -2,7 +2,6 @@ import React, {useEffect, useRef, useState} from "react"; import { useOutletContext } from "react-router-dom"; import {AlertError, Loading, RefreshButton} from "../../../../lib/components/controls"; import {useRefs} from "../../../../lib/hooks/repo"; -import Card from "react-bootstrap/Card"; import {Button, ListGroup, Row} from "react-bootstrap"; import Col from "react-bootstrap/Col"; import {useAPI} from "../../../../lib/hooks/api"; @@ -11,6 +10,25 @@ import Modal from "react-bootstrap/Modal"; import Form from "react-bootstrap/Form"; import Alert from "react-bootstrap/Alert"; +const BranchProtectionRulesList = ({ rulesResponse, deleteButtonDisabled, onDeleteRule }) => { + if (!rulesResponse) return null; + + return ( +
+ + {rulesResponse['rules'].length > 0 ? rulesResponse['rules'].map((r) => { + return +
+ {r.pattern} + +
+
+ }) : There aren't any rules yet.} +
+
+ ); +}; + const SettingsContainer = () => { const {repo, loading, error} = useRefs(); const [showCreateModal, setShowCreateModal] = useState(false); @@ -53,23 +71,14 @@ const SettingsContainer = () => { {/* eslint-disable-next-line react/jsx-no-target-blank */} Learn more.
- {loading || rulesLoading ?
: -
- - - - {rulesResponse && rulesResponse['rules'].length > 0 ? rulesResponse['rules'].map((r) => { - return -
- {r.pattern} - -
-
- }) : There aren't any rules yet.} -
-
-
-
} +
+ {loading || rulesLoading ? : + } +
setShowCreateModal(false)} currentRulesResponse={rulesResponse} onSuccess={() => { setRefresh(!refresh) diff --git a/webui/src/pages/repositories/repository/settings/retention.jsx b/webui/src/pages/repositories/repository/settings/retention.jsx index 7903bf23ccb..b4f6309cd2e 100644 --- a/webui/src/pages/repositories/repository/settings/retention.jsx +++ b/webui/src/pages/repositories/repository/settings/retention.jsx @@ -16,6 +16,7 @@ import Card from "react-bootstrap/Card"; import Table from "react-bootstrap/Table"; import {PolicyEditor} from "../../../../lib/components/policy"; import Alert from "react-bootstrap/Alert"; +import { LightBulbIcon } from "@primer/octicons-react"; const exampleJson = (defaultBranch) => { return { @@ -85,13 +86,10 @@ const GCPolicy = ({repo}) => { } else { content = <> - - - - - - -
Default retention days: {policy.default_retention_days}
+ + + Default retention days: {policy.default_retention_days} + {policy.branches && @@ -138,11 +136,11 @@ const GCPolicy = ({repo}) => { -
+

{/* eslint-disable-next-line react/jsx-no-target-blank */} This policy determines for how long objects are kept in the storage after they are deleted in lakeFS. Learn more. -

+

{content}
diff --git a/webui/src/styles/components/ui-components.css b/webui/src/styles/components/ui-components.css index fdfe231baed..8ae6b3767f5 100644 --- a/webui/src/styles/components/ui-components.css +++ b/webui/src/styles/components/ui-components.css @@ -44,6 +44,16 @@ border-color: var(--danger-light); } +.alert-error-body { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.alert-error-body > div:first-child { + margin-right: 1rem; +} + /* Dark mode alert fixes */ [data-bs-theme="dark"] .alert-info { background-color: #1e3a8a; From dd429e887e65860fc1a9290a5e42196d5f6ba6ab Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Wed, 28 May 2025 11:29:47 -0400 Subject: [PATCH 7/8] fixed protected branch error --- webui/src/pages/repositories/repository/objects.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/src/pages/repositories/repository/objects.jsx b/webui/src/pages/repositories/repository/objects.jsx index 0a91bc9239b..06f40166307 100644 --- a/webui/src/pages/repositories/repository/objects.jsx +++ b/webui/src/pages/repositories/repository/objects.jsx @@ -48,7 +48,7 @@ const REPOSITORY_AGE_BEFORE_GC = 14; const MAX_PARALLEL_UPLOADS = 5; const isAbortedError = (error, controller) => { - return (error instanceof DOMException && error.name === 'AbortError') || controller.signal.aborted; + return (error instanceof DOMException && error.name === 'AbortError') || controller?.signal?.aborted; }; const ImportButton = ({ variant = "success", onClick, config }) => { @@ -695,7 +695,7 @@ const UploadButton = ({config, repo, reference, path, onDone, onClick, onHide, s )} {(uploadState.error && !isAbortedError(uploadState.error, abortController)) && - setUploadState(prev => ({...prev, error: null}))}/> + setUploadState(prev => ({...prev, error: null}))}/> } From 83e9790cc7aae493bcdc88cf3c69fd7619aaf81b Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Thu, 29 May 2025 17:02:50 -0400 Subject: [PATCH 8/8] fix review comments --- .../src/lib/components/repository/dataTable.jsx | 6 +++--- webui/src/lib/components/repository/tree.jsx | 2 +- webui/src/lib/components/repository/treeRows.jsx | 4 ++-- .../repository/fileRenderers/simple.tsx | 1 - .../repository/settings/branches.jsx | 16 ++++++++-------- webui/src/styles/objects/objects.css | 4 ++++ 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/webui/src/lib/components/repository/dataTable.jsx b/webui/src/lib/components/repository/dataTable.jsx index 6f10dc47afe..64bb5e15e3e 100644 --- a/webui/src/lib/components/repository/dataTable.jsx +++ b/webui/src/lib/components/repository/dataTable.jsx @@ -106,14 +106,14 @@ DataTable.propTypes = { columns: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string, - Header: PropTypes.string.isRequired, accessor: PropTypes.string, + className: PropTypes.string, + description: PropTypes.string, + Header: PropTypes.string.isRequired, Cell: PropTypes.func, // className applies to the header cell (th element) - className: PropTypes.string, // cellClassName applies to all data cells (td elements) in this column cellClassName: PropTypes.string, - description: PropTypes.string, }) ).isRequired, data: PropTypes.array.isRequired, diff --git a/webui/src/lib/components/repository/tree.jsx b/webui/src/lib/components/repository/tree.jsx index a42440c841d..6707101a0fe 100644 --- a/webui/src/lib/components/repository/tree.jsx +++ b/webui/src/lib/components/repository/tree.jsx @@ -586,7 +586,7 @@ const EntryRow = ({ config, repo, reference, path, entry, onDelete, showActions return ( <> - + {diffIndicator && }
{diffIndicator}{diffIndicator} {entry.path_type === "common_prefix" ? ( diff --git a/webui/src/lib/components/repository/treeRows.jsx b/webui/src/lib/components/repository/treeRows.jsx index f174baa3e08..f295d194d05 100644 --- a/webui/src/lib/components/repository/treeRows.jsx +++ b/webui/src/lib/components/repository/treeRows.jsx @@ -35,7 +35,7 @@ class RowAction { const ChangeRowActions = ({actions}) => <> { actions.map(action => ( - <> ))} ; diff --git a/webui/src/pages/repositories/repository/fileRenderers/simple.tsx b/webui/src/pages/repositories/repository/fileRenderers/simple.tsx index 6e9f3727591..73a874f7d58 100644 --- a/webui/src/pages/repositories/repository/fileRenderers/simple.tsx +++ b/webui/src/pages/repositories/repository/fileRenderers/simple.tsx @@ -88,7 +88,6 @@ export const TextRenderer: FC = ({ return ( +
{rulesResponse['rules'].length > 0 ? rulesResponse['rules'].map((r) => { return @@ -56,12 +56,12 @@ const SettingsContainer = () => { if (actionError) return ; return (<>
-
-

-
+
+

+
Branch protection rules
- {setRefresh(!refresh)}}/> - + {setRefresh(!refresh)}}/> +

@@ -71,7 +71,7 @@ const SettingsContainer = () => { {/* eslint-disable-next-line react/jsx-no-target-blank */} Learn more.
-
+
{loading || rulesLoading ? : Create Branch Protection Rule - +
{ e.preventDefault(); createRule(patternField.current.value); diff --git a/webui/src/styles/objects/objects.css b/webui/src/styles/objects/objects.css index 8fddde2f289..0d894050951 100644 --- a/webui/src/styles/objects/objects.css +++ b/webui/src/styles/objects/objects.css @@ -18,6 +18,10 @@ padding: 0; } +.tree-container table > tbody > tr > td { + padding-left: 30px; +} + .tree-entry-row, .change-entry-row { transition: background-color var(--transition-fast);