Replace project cards with sortable data table on Home page
This commit is contained in:
@@ -35,7 +35,7 @@ kics:
|
|||||||
image: deps.global.bsf.tools/docker/python:3.12-slim
|
image: deps.global.bsf.tools/docker/python:3.12-slim
|
||||||
timeout: 10m
|
timeout: 10m
|
||||||
before_script:
|
before_script:
|
||||||
- pip install httpx
|
- pip install --index-url "$PIP_INDEX_URL" httpx
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
python - <<'PYTEST_SCRIPT'
|
python - <<'PYTEST_SCRIPT'
|
||||||
@@ -175,7 +175,7 @@ frontend_tests:
|
|||||||
# Shared deploy configuration
|
# Shared deploy configuration
|
||||||
.deploy_template: &deploy_template
|
.deploy_template: &deploy_template
|
||||||
stage: deploy
|
stage: deploy
|
||||||
needs: [build_image, kics, hadolint, python_tests, frontend_tests]
|
needs: [build_image, kics, hadolint, python_tests, frontend_tests, secrets]
|
||||||
image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
|
image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
|
||||||
|
|
||||||
.helm_setup: &helm_setup
|
.helm_setup: &helm_setup
|
||||||
@@ -245,6 +245,7 @@ deploy_stage:
|
|||||||
-f $VALUES_FILE \
|
-f $VALUES_FILE \
|
||||||
--set image.tag=git.linux-amd64-$CI_COMMIT_SHA \
|
--set image.tag=git.linux-amd64-$CI_COMMIT_SHA \
|
||||||
--wait \
|
--wait \
|
||||||
|
--atomic \
|
||||||
--timeout 5m
|
--timeout 5m
|
||||||
- kubectl rollout status deployment/orchard-stage-server -n $NAMESPACE --timeout=5m
|
- kubectl rollout status deployment/orchard-stage-server -n $NAMESPACE --timeout=5m
|
||||||
- *verify_deployment
|
- *verify_deployment
|
||||||
@@ -280,6 +281,7 @@ deploy_feature:
|
|||||||
--set minioIngress.host=minio-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \
|
--set minioIngress.host=minio-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \
|
||||||
--set minioIngress.tls.secretName=minio-$CI_COMMIT_REF_SLUG-tls \
|
--set minioIngress.tls.secretName=minio-$CI_COMMIT_REF_SLUG-tls \
|
||||||
--wait \
|
--wait \
|
||||||
|
--atomic \
|
||||||
--timeout 5m
|
--timeout 5m
|
||||||
- kubectl rollout status deployment/orchard-$CI_COMMIT_REF_SLUG-server -n $NAMESPACE --timeout=5m
|
- kubectl rollout status deployment/orchard-$CI_COMMIT_REF_SLUG-server -n $NAMESPACE --timeout=5m
|
||||||
- export BASE_URL="https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools"
|
- export BASE_URL="https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools"
|
||||||
|
|||||||
@@ -5,3 +5,9 @@
|
|||||||
# These are historical commits - files have since been deleted or updated with inline comments
|
# These are historical commits - files have since been deleted or updated with inline comments
|
||||||
7e68baed0886a3c928644cd01aa3b39f92d4f976:backend/tests/test_duplicate_detection.py:generic-api-key:154
|
7e68baed0886a3c928644cd01aa3b39f92d4f976:backend/tests/test_duplicate_detection.py:generic-api-key:154
|
||||||
2f1891cf0126ec0e7d4c789d872a2cb2dd3a1745:backend/tests/unit/test_storage.py:generic-api-key:381
|
2f1891cf0126ec0e7d4c789d872a2cb2dd3a1745:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
10d36947948de796f0bacea3827f4531529c405d:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
bccbc71c13570d14b8b26a11335c45f102fe3072:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
5c9da9003b844a2d655cce74a7c82c57e74f27c4:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
90bb2a3a393d2361dc3136ee8d761debb0726d8a:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
37666e41a72d2a4f34447c0d1a8728e1d7271d24:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
0cc4f253621a9601c5193f6ae1e7ae33f0e7fc9b:backend/tests/unit/test_storage.py:generic-api-key:381
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -16,13 +16,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Added internal proxy configuration for npm, pip, helm, and apt (#51)
|
- Added internal proxy configuration for npm, pip, helm, and apt (#51)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Added `--atomic` flag to Helm deployments for automatic rollback on failure
|
||||||
- Adjusted dark mode color palette to use lighter background tones for better readability and reduced eye strain (#52)
|
- Adjusted dark mode color palette to use lighter background tones for better readability and reduced eye strain (#52)
|
||||||
|
- Replaced project card grid with sortable data table on Home page for better handling of large project lists
|
||||||
|
- Replaced package card grid with sortable data table on Project page for consistency
|
||||||
|
- Replaced SortDropdown with table header sorting on Package page for consistency
|
||||||
|
- Enabled sorting on supported table columns (name, created, updated) via clickable headers
|
||||||
|
- Updated browser tab title to "Orchard" with custom favicon
|
||||||
- Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51)
|
- Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed `cleanup_feature` job failing when branch is deleted (`GIT_STRATEGY: none`) (#51)
|
- Fixed `cleanup_feature` job failing when branch is deleted (`GIT_STRATEGY: none`) (#51)
|
||||||
- Fixed gitleaks false positives with fingerprints for historical commits (#51)
|
- Fixed gitleaks false positives with fingerprints for historical commits (#51)
|
||||||
- Fixed integration tests running when deploy fails (`when: on_success`) (#51)
|
- Fixed integration tests running when deploy fails (`when: on_success`) (#51)
|
||||||
|
- Fixed static file serving for favicon and other files in frontend dist root
|
||||||
|
- Fixed deploy jobs running when secrets scan fails (added `secrets` to deploy dependencies)
|
||||||
|
- Fixed dev environment memory requests to equal limits per cluster Kyverno policy
|
||||||
|
- Fixed init containers missing resource limits (Kyverno policy compliance)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Removed unused `store_streaming()` method from storage.py (#51)
|
- Removed unused `store_streaming()` method from storage.py (#51)
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ if os.path.exists(static_dir):
|
|||||||
|
|
||||||
raise HTTPException(status_code=404, detail="Not found")
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
|
# Check if requesting a static file from dist root (favicon, etc.)
|
||||||
|
static_file_path = os.path.join(static_dir, full_path)
|
||||||
|
if os.path.isfile(static_file_path) and not full_path.startswith("."):
|
||||||
|
return FileResponse(static_file_path)
|
||||||
|
|
||||||
# Serve SPA for all other routes (including /project/*)
|
# Serve SPA for all other routes (including /project/*)
|
||||||
index_path = os.path.join(static_dir, "index.html")
|
index_path = os.path.join(static_dir, "index.html")
|
||||||
if os.path.exists(index_path):
|
if os.path.exists(index_path):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.local
|
dockerfile: Dockerfile.local
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "0.0.0.0:8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- ORCHARD_SERVER_HOST=0.0.0.0
|
- ORCHARD_SERVER_HOST=0.0.0.0
|
||||||
- ORCHARD_SERVER_PORT=8080
|
- ORCHARD_SERVER_PORT=8080
|
||||||
@@ -71,10 +71,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- orchard-network
|
- orchard-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -100,10 +96,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- orchard-network
|
- orchard-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -124,10 +116,6 @@ services:
|
|||||||
"
|
"
|
||||||
networks:
|
networks:
|
||||||
- orchard-network
|
- orchard-network
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -149,10 +137,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- orchard-network
|
- orchard-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
cap_drop:
|
|
||||||
- ALL
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/orchard.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Orchard - Content-Addressable Storage</title>
|
<title>Orchard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
18
frontend/public/orchard.svg
Normal file
18
frontend/public/orchard.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Green background -->
|
||||||
|
<rect width="24" height="24" rx="4" fill="#4CAF50"/>
|
||||||
|
<!-- Three fruit trees representing an orchard - shifted down to center -->
|
||||||
|
<g transform="translate(0, 2)">
|
||||||
|
<!-- Left tree - rounded canopy -->
|
||||||
|
<path d="M6 14 Q6 8 3 8 Q6 4 6 4 Q6 4 9 8 Q6 8 6 14" fill="white" opacity="0.7"/>
|
||||||
|
<rect x="5.25" y="13" width="1.5" height="4" fill="white" opacity="0.7"/>
|
||||||
|
<!-- Center tree - larger rounded canopy -->
|
||||||
|
<path d="M12 12 Q12 5 8 5 Q12 1 12 1 Q12 1 16 5 Q12 5 12 12" fill="white"/>
|
||||||
|
<rect x="11.25" y="11" width="1.5" height="5" fill="white"/>
|
||||||
|
<!-- Right tree - rounded canopy -->
|
||||||
|
<path d="M18 14 Q18 8 15 8 Q18 4 18 4 Q18 4 21 8 Q18 8 18 14" fill="white" opacity="0.7"/>
|
||||||
|
<rect x="17.25" y="13" width="1.5" height="4" fill="white" opacity="0.7"/>
|
||||||
|
<!-- Ground -->
|
||||||
|
<ellipse cx="12" cy="18" rx="8" ry="1.5" fill="white" opacity="0.4"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1012 B |
@@ -98,3 +98,58 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Clickable rows */
|
||||||
|
.data-table__row--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table__row--clickable:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive table wrapper */
|
||||||
|
.data-table--responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table--responsive table {
|
||||||
|
min-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cell with name and icon */
|
||||||
|
.data-table .cell-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table .cell-name:hover {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date cells */
|
||||||
|
.data-table .cell-date {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Description cell */
|
||||||
|
.data-table .cell-description {
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Owner cell */
|
||||||
|
.data-table .cell-owner {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface DataTableProps<T> {
|
|||||||
onSort?: (key: string) => void;
|
onSort?: (key: string) => void;
|
||||||
sortKey?: string;
|
sortKey?: string;
|
||||||
sortOrder?: 'asc' | 'desc';
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
onRowClick?: (item: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<T>({
|
export function DataTable<T>({
|
||||||
@@ -29,6 +30,7 @@ export function DataTable<T>({
|
|||||||
onSort,
|
onSort,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
onRowClick,
|
||||||
}: DataTableProps<T>) {
|
}: DataTableProps<T>) {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +73,11 @@ export function DataTable<T>({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((item) => (
|
{data.map((item) => (
|
||||||
<tr key={keyExtractor(item)}>
|
<tr
|
||||||
|
key={keyExtractor(item)}
|
||||||
|
onClick={() => onRowClick?.(item)}
|
||||||
|
className={onRowClick ? 'data-table__row--clickable' : ''}
|
||||||
|
>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<td key={column.key} className={column.className}>
|
<td key={column.key} className={column.className}>
|
||||||
{column.render(item)}
|
{column.render(item)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* Page Layout */
|
/* Page Layout */
|
||||||
.home {
|
.home {
|
||||||
max-width: 1000px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import { Project, PaginatedResponse } from '../types';
|
import { Project, PaginatedResponse } from '../types';
|
||||||
import { listProjects, createProject } from '../api';
|
import { listProjects, createProject } from '../api';
|
||||||
import { Badge } from '../components/Badge';
|
import { Badge } from '../components/Badge';
|
||||||
import { SortDropdown, SortOption } from '../components/SortDropdown';
|
import { DataTable } from '../components/DataTable';
|
||||||
import { FilterDropdown, FilterOption } from '../components/FilterDropdown';
|
import { FilterDropdown, FilterOption } from '../components/FilterDropdown';
|
||||||
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||||
import { Pagination } from '../components/Pagination';
|
import { Pagination } from '../components/Pagination';
|
||||||
@@ -20,12 +20,6 @@ function LockIcon() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SORT_OPTIONS: SortOption[] = [
|
|
||||||
{ value: 'name', label: 'Name' },
|
|
||||||
{ value: 'created_at', label: 'Created' },
|
|
||||||
{ value: 'updated_at', label: 'Updated' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const VISIBILITY_OPTIONS: FilterOption[] = [
|
const VISIBILITY_OPTIONS: FilterOption[] = [
|
||||||
{ value: '', label: 'All Projects' },
|
{ value: '', label: 'All Projects' },
|
||||||
{ value: 'public', label: 'Public Only' },
|
{ value: 'public', label: 'Public Only' },
|
||||||
@@ -34,6 +28,7 @@ const VISIBILITY_OPTIONS: FilterOption[] = [
|
|||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const [projectsData, setProjectsData] = useState<PaginatedResponse<Project> | null>(null);
|
const [projectsData, setProjectsData] = useState<PaginatedResponse<Project> | null>(null);
|
||||||
@@ -101,8 +96,10 @@ function Home() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => {
|
const handleSortChange = (columnKey: string) => {
|
||||||
updateParams({ sort: newSort, order: newOrder, page: '1' });
|
// Toggle order if clicking the same column, otherwise default to asc
|
||||||
|
const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc';
|
||||||
|
updateParams({ sort: columnKey, order: newOrder, page: '1' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVisibilityChange = (value: string) => {
|
const handleVisibilityChange = (value: string) => {
|
||||||
@@ -189,7 +186,6 @@ function Home() {
|
|||||||
value={visibility}
|
value={visibility}
|
||||||
onChange={handleVisibilityChange}
|
onChange={handleVisibilityChange}
|
||||||
/>
|
/>
|
||||||
<SortDropdown options={SORT_OPTIONS} value={sort} order={order} onChange={handleSortChange} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
@@ -204,30 +200,59 @@ function Home() {
|
|||||||
</FilterChipGroup>
|
</FilterChipGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projects.length === 0 ? (
|
<div className="data-table--responsive">
|
||||||
<div className="empty-state">
|
<DataTable
|
||||||
{hasActiveFilters ? (
|
data={projects}
|
||||||
<p>No projects match your filters. Try adjusting your search.</p>
|
keyExtractor={(project) => project.id}
|
||||||
) : (
|
onRowClick={(project) => navigate(`/project/${project.name}`)}
|
||||||
<p>No projects yet. Create your first project to get started!</p>
|
onSort={handleSortChange}
|
||||||
)}
|
sortKey={sort}
|
||||||
</div>
|
sortOrder={order}
|
||||||
) : (
|
emptyMessage={
|
||||||
<>
|
hasActiveFilters
|
||||||
<div className="project-grid">
|
? 'No projects match your filters. Try adjusting your search.'
|
||||||
{projects.map((project) => (
|
: 'No projects yet. Create your first project to get started!'
|
||||||
<Link to={`/project/${project.name}`} key={project.id} className="project-card card">
|
}
|
||||||
<h3>
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
sortable: true,
|
||||||
|
render: (project) => (
|
||||||
|
<span className="cell-name">
|
||||||
{!project.is_public && <LockIcon />}
|
{!project.is_public && <LockIcon />}
|
||||||
{project.name}
|
{project.name}
|
||||||
</h3>
|
</span>
|
||||||
{project.description && <p>{project.description}</p>}
|
),
|
||||||
<div className="project-meta">
|
},
|
||||||
<div className="project-badges">
|
{
|
||||||
|
key: 'description',
|
||||||
|
header: 'Description',
|
||||||
|
className: 'cell-description',
|
||||||
|
render: (project) => project.description || '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'visibility',
|
||||||
|
header: 'Visibility',
|
||||||
|
render: (project) => (
|
||||||
<Badge variant={project.is_public ? 'public' : 'private'}>
|
<Badge variant={project.is_public ? 'public' : 'private'}>
|
||||||
{project.is_public ? 'Public' : 'Private'}
|
{project.is_public ? 'Public' : 'Private'}
|
||||||
</Badge>
|
</Badge>
|
||||||
{user && project.access_level && (
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_by',
|
||||||
|
header: 'Owner',
|
||||||
|
className: 'cell-owner',
|
||||||
|
render: (project) => project.created_by,
|
||||||
|
},
|
||||||
|
...(user
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: 'access_level',
|
||||||
|
header: 'Access',
|
||||||
|
render: (project: Project) =>
|
||||||
|
project.access_level ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
project.is_owner
|
project.is_owner
|
||||||
@@ -239,22 +264,32 @@ function Home() {
|
|||||||
: 'default'
|
: 'default'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{project.is_owner ? 'Owner' : project.access_level.charAt(0).toUpperCase() + project.access_level.slice(1)}
|
{project.is_owner
|
||||||
|
? 'Owner'
|
||||||
|
: project.access_level.charAt(0).toUpperCase() + project.access_level.slice(1)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : (
|
||||||
</div>
|
'—'
|
||||||
<div className="project-meta__dates">
|
),
|
||||||
<span className="date">Created {new Date(project.created_at).toLocaleDateString()}</span>
|
},
|
||||||
{project.updated_at !== project.created_at && (
|
]
|
||||||
<span className="date">Updated {new Date(project.updated_at).toLocaleDateString()}</span>
|
: []),
|
||||||
)}
|
{
|
||||||
</div>
|
key: 'created_at',
|
||||||
</div>
|
header: 'Created',
|
||||||
<div className="project-meta__owner">
|
sortable: true,
|
||||||
<span className="owner">by {project.created_by}</span>
|
className: 'cell-date',
|
||||||
</div>
|
render: (project) => new Date(project.created_at).toLocaleDateString(),
|
||||||
</Link>
|
},
|
||||||
))}
|
{
|
||||||
|
key: 'updated_at',
|
||||||
|
header: 'Updated',
|
||||||
|
sortable: true,
|
||||||
|
className: 'cell-date',
|
||||||
|
render: (project) => new Date(project.updated_at).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pagination && pagination.total_pages > 1 && (
|
{pagination && pagination.total_pages > 1 && (
|
||||||
@@ -266,8 +301,6 @@ function Home() {
|
|||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { listTags, getDownloadUrl, getPackage, getMyProjectAccess, UnauthorizedE
|
|||||||
import { Breadcrumb } from '../components/Breadcrumb';
|
import { Breadcrumb } from '../components/Breadcrumb';
|
||||||
import { Badge } from '../components/Badge';
|
import { Badge } from '../components/Badge';
|
||||||
import { SearchInput } from '../components/SearchInput';
|
import { SearchInput } from '../components/SearchInput';
|
||||||
import { SortDropdown, SortOption } from '../components/SortDropdown';
|
|
||||||
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||||
import { DataTable } from '../components/DataTable';
|
import { DataTable } from '../components/DataTable';
|
||||||
import { Pagination } from '../components/Pagination';
|
import { Pagination } from '../components/Pagination';
|
||||||
@@ -14,11 +13,6 @@ import { useAuth } from '../contexts/AuthContext';
|
|||||||
import './Home.css';
|
import './Home.css';
|
||||||
import './PackagePage.css';
|
import './PackagePage.css';
|
||||||
|
|
||||||
const SORT_OPTIONS: SortOption[] = [
|
|
||||||
{ value: 'name', label: 'Name' },
|
|
||||||
{ value: 'created_at', label: 'Created' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -164,8 +158,9 @@ function PackagePage() {
|
|||||||
updateParams({ search: value, page: '1' });
|
updateParams({ search: value, page: '1' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => {
|
const handleSortChange = (columnKey: string) => {
|
||||||
updateParams({ sort: newSort, order: newOrder, page: '1' });
|
const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc';
|
||||||
|
updateParams({ sort: columnKey, order: newOrder, page: '1' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
@@ -198,19 +193,19 @@ function PackagePage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'size',
|
key: 'artifact_size',
|
||||||
header: 'Size',
|
header: 'Size',
|
||||||
render: (t: TagDetail) => <span>{formatBytes(t.artifact_size)}</span>,
|
render: (t: TagDetail) => <span>{formatBytes(t.artifact_size)}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'content_type',
|
key: 'artifact_content_type',
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
render: (t: TagDetail) => (
|
render: (t: TagDetail) => (
|
||||||
<span className="content-type">{t.artifact_content_type || '-'}</span>
|
<span className="content-type">{t.artifact_content_type || '-'}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'original_name',
|
key: 'artifact_original_name',
|
||||||
header: 'Filename',
|
header: 'Filename',
|
||||||
className: 'cell-truncate',
|
className: 'cell-truncate',
|
||||||
render: (t: TagDetail) => (
|
render: (t: TagDetail) => (
|
||||||
@@ -376,7 +371,6 @@ function PackagePage() {
|
|||||||
placeholder="Filter tags..."
|
placeholder="Filter tags..."
|
||||||
className="list-controls__search"
|
className="list-controls__search"
|
||||||
/>
|
/>
|
||||||
<SortDropdown options={SORT_OPTIONS} value={sort} order={order} onChange={handleSortChange} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
@@ -385,6 +379,7 @@ function PackagePage() {
|
|||||||
</FilterChipGroup>
|
</FilterChipGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="data-table--responsive">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={tags}
|
data={tags}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -394,16 +389,11 @@ function PackagePage() {
|
|||||||
? 'No tags match your filters. Try adjusting your search.'
|
? 'No tags match your filters. Try adjusting your search.'
|
||||||
: 'No tags yet. Upload an artifact with a tag to create one!'
|
: 'No tags yet. Upload an artifact with a tag to create one!'
|
||||||
}
|
}
|
||||||
onSort={(key) => {
|
onSort={handleSortChange}
|
||||||
if (key === sort) {
|
|
||||||
handleSortChange(key, order === 'asc' ? 'desc' : 'asc');
|
|
||||||
} else {
|
|
||||||
handleSortChange(key, 'asc');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sortKey={sort}
|
sortKey={sort}
|
||||||
sortOrder={order}
|
sortOrder={order}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{pagination && pagination.total_pages > 1 && (
|
{pagination && pagination.total_pages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useParams, Link, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { Project, Package, PaginatedResponse, AccessLevel } from '../types';
|
import { Project, Package, PaginatedResponse, AccessLevel } from '../types';
|
||||||
import { getProject, listPackages, createPackage, getMyProjectAccess, UnauthorizedError, ForbiddenError } from '../api';
|
import { getProject, listPackages, createPackage, getMyProjectAccess, UnauthorizedError, ForbiddenError } from '../api';
|
||||||
import { Breadcrumb } from '../components/Breadcrumb';
|
import { Breadcrumb } from '../components/Breadcrumb';
|
||||||
import { Badge } from '../components/Badge';
|
import { Badge } from '../components/Badge';
|
||||||
|
import { DataTable } from '../components/DataTable';
|
||||||
import { SearchInput } from '../components/SearchInput';
|
import { SearchInput } from '../components/SearchInput';
|
||||||
import { SortDropdown, SortOption } from '../components/SortDropdown';
|
|
||||||
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||||
import { Pagination } from '../components/Pagination';
|
import { Pagination } from '../components/Pagination';
|
||||||
import { AccessManagement } from '../components/AccessManagement';
|
import { AccessManagement } from '../components/AccessManagement';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import './Home.css';
|
import './Home.css';
|
||||||
|
|
||||||
const SORT_OPTIONS: SortOption[] = [
|
|
||||||
{ value: 'name', label: 'Name' },
|
|
||||||
{ value: 'created_at', label: 'Created' },
|
|
||||||
{ value: 'updated_at', label: 'Updated' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FORMAT_OPTIONS = ['generic', 'npm', 'pypi', 'docker', 'deb', 'rpm', 'maven', 'nuget', 'helm'];
|
const FORMAT_OPTIONS = ['generic', 'npm', 'pypi', 'docker', 'deb', 'rpm', 'maven', 'nuget', 'helm'];
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
@@ -140,8 +134,9 @@ function ProjectPage() {
|
|||||||
updateParams({ search: value, page: '1' });
|
updateParams({ search: value, page: '1' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => {
|
const handleSortChange = (columnKey: string) => {
|
||||||
updateParams({ sort: newSort, order: newOrder, page: '1' });
|
const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc';
|
||||||
|
updateParams({ sort: columnKey, order: newOrder, page: '1' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormatChange = (value: string) => {
|
const handleFormatChange = (value: string) => {
|
||||||
@@ -294,7 +289,6 @@ function ProjectPage() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<SortDropdown options={SORT_OPTIONS} value={sort} order={order} onChange={handleSortChange} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
@@ -304,58 +298,68 @@ function ProjectPage() {
|
|||||||
</FilterChipGroup>
|
</FilterChipGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{packages.length === 0 ? (
|
<div className="data-table--responsive">
|
||||||
<div className="empty-state">
|
<DataTable
|
||||||
{hasActiveFilters ? (
|
data={packages}
|
||||||
<p>No packages match your filters. Try adjusting your search.</p>
|
keyExtractor={(pkg) => pkg.id}
|
||||||
) : (
|
onRowClick={(pkg) => navigate(`/project/${projectName}/${pkg.name}`)}
|
||||||
<p>No packages yet. Create your first package to start uploading artifacts!</p>
|
onSort={handleSortChange}
|
||||||
)}
|
sortKey={sort}
|
||||||
</div>
|
sortOrder={order}
|
||||||
) : (
|
emptyMessage={
|
||||||
<>
|
hasActiveFilters
|
||||||
<div className="project-grid">
|
? 'No packages match your filters. Try adjusting your search.'
|
||||||
{packages.map((pkg) => (
|
: 'No packages yet. Create your first package to start uploading artifacts!'
|
||||||
<Link to={`/project/${projectName}/${pkg.name}`} key={pkg.id} className="project-card card">
|
}
|
||||||
<div className="package-card__header">
|
columns={[
|
||||||
<h3>{pkg.name}</h3>
|
{
|
||||||
<Badge variant="default">{pkg.format}</Badge>
|
key: 'name',
|
||||||
</div>
|
header: 'Name',
|
||||||
{pkg.description && <p>{pkg.description}</p>}
|
sortable: true,
|
||||||
|
render: (pkg) => <span className="cell-name">{pkg.name}</span>,
|
||||||
{(pkg.tag_count !== undefined || pkg.artifact_count !== undefined) && (
|
},
|
||||||
<div className="package-stats">
|
{
|
||||||
{pkg.tag_count !== undefined && (
|
key: 'description',
|
||||||
<div className="package-stats__item">
|
header: 'Description',
|
||||||
<span className="package-stats__value">{pkg.tag_count}</span>
|
className: 'cell-description',
|
||||||
<span className="package-stats__label">Tags</span>
|
render: (pkg) => pkg.description || '—',
|
||||||
</div>
|
},
|
||||||
)}
|
{
|
||||||
{pkg.artifact_count !== undefined && (
|
key: 'format',
|
||||||
<div className="package-stats__item">
|
header: 'Format',
|
||||||
<span className="package-stats__value">{pkg.artifact_count}</span>
|
render: (pkg) => <Badge variant="default">{pkg.format}</Badge>,
|
||||||
<span className="package-stats__label">Artifacts</span>
|
},
|
||||||
</div>
|
{
|
||||||
)}
|
key: 'tag_count',
|
||||||
{pkg.total_size !== undefined && pkg.total_size > 0 && (
|
header: 'Tags',
|
||||||
<div className="package-stats__item">
|
render: (pkg) => pkg.tag_count ?? '—',
|
||||||
<span className="package-stats__value">{formatBytes(pkg.total_size)}</span>
|
},
|
||||||
<span className="package-stats__label">Size</span>
|
{
|
||||||
</div>
|
key: 'artifact_count',
|
||||||
)}
|
header: 'Artifacts',
|
||||||
</div>
|
render: (pkg) => pkg.artifact_count ?? '—',
|
||||||
)}
|
},
|
||||||
|
{
|
||||||
<div className="project-meta">
|
key: 'total_size',
|
||||||
{pkg.latest_tag && (
|
header: 'Size',
|
||||||
<span className="latest-tag">
|
render: (pkg) =>
|
||||||
Latest: <strong>{pkg.latest_tag}</strong>
|
pkg.total_size !== undefined && pkg.total_size > 0 ? formatBytes(pkg.total_size) : '—',
|
||||||
</span>
|
},
|
||||||
)}
|
{
|
||||||
<span className="date">Created {new Date(pkg.created_at).toLocaleDateString()}</span>
|
key: 'latest_tag',
|
||||||
</div>
|
header: 'Latest',
|
||||||
</Link>
|
render: (pkg) =>
|
||||||
))}
|
pkg.latest_tag ? <strong style={{ color: 'var(--accent-primary)' }}>{pkg.latest_tag}</strong> : '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'created_at',
|
||||||
|
header: 'Created',
|
||||||
|
sortable: true,
|
||||||
|
className: 'cell-date',
|
||||||
|
render: (pkg) => new Date(pkg.created_at).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pagination && pagination.total_pages > 1 && (
|
{pagination && pagination.total_pages > 1 && (
|
||||||
@@ -367,8 +371,6 @@ function ProjectPage() {
|
|||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canAdmin && projectName && (
|
{canAdmin && projectName && (
|
||||||
<AccessManagement projectName={projectName} />
|
<AccessManagement projectName={projectName} />
|
||||||
|
|||||||
@@ -37,12 +37,26 @@ spec:
|
|||||||
image: "{{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }}"
|
image: "{{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }}"
|
||||||
imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }}
|
||||||
command: ['sh', '-c', 'until nc -z {{ include "orchard.postgresql.host" . }} 5432; do echo waiting for database; sleep 2; done;']
|
command: ['sh', '-c', 'until nc -z {{ include "orchard.postgresql.host" . }} 5432; do echo waiting for database; sleep 2; done;']
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 32Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.minio.enabled }}
|
{{- if .Values.minio.enabled }}
|
||||||
- name: wait-for-minio
|
- name: wait-for-minio
|
||||||
image: "{{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }}"
|
image: "{{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }}"
|
||||||
imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }}
|
||||||
command: ['sh', '-c', 'until nc -z {{ .Release.Name }}-minio 9000; do echo waiting for minio; sleep 2; done;']
|
command: ['sh', '-c', 'until nc -z {{ .Release.Name }}-minio 9000; do echo waiting for minio; sleep 2; done;']
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 32Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
{{- end }}
|
{{- end }}
|
||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
|
|||||||
@@ -53,13 +53,14 @@ ingress:
|
|||||||
- orchard-dev.common.global.bsf.tools # Overridden by CI
|
- orchard-dev.common.global.bsf.tools # Overridden by CI
|
||||||
|
|
||||||
# Lighter resources for ephemeral environments
|
# Lighter resources for ephemeral environments
|
||||||
|
# Note: memory requests must equal limits per cluster policy
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 250m
|
cpu: 250m
|
||||||
memory: 256Mi
|
memory: 256Mi
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 128Mi
|
memory: 256Mi
|
||||||
|
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
@@ -127,6 +128,25 @@ postgresql:
|
|||||||
primary:
|
primary:
|
||||||
persistence:
|
persistence:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
# Resources with memory requests = limits per cluster policy
|
||||||
|
resourcesPreset: "none"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
# Volume permissions init container
|
||||||
|
volumePermissions:
|
||||||
|
resourcesPreset: "none"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 64Mi
|
||||||
|
|
||||||
# MinIO - ephemeral, no persistence
|
# MinIO - ephemeral, no persistence
|
||||||
minio:
|
minio:
|
||||||
@@ -142,6 +162,35 @@ minio:
|
|||||||
defaultBuckets: "orchard-artifacts"
|
defaultBuckets: "orchard-artifacts"
|
||||||
persistence:
|
persistence:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
# Resources with memory requests = limits per cluster policy
|
||||||
|
resourcesPreset: "none" # Disable preset to use explicit resources
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
# Init container resources
|
||||||
|
defaultInitContainers:
|
||||||
|
volumePermissions:
|
||||||
|
resourcesPreset: "none"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 64Mi
|
||||||
|
# Provisioning job resources
|
||||||
|
provisioning:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
|
||||||
# MinIO ingress - hostname overridden by CI
|
# MinIO ingress - hostname overridden by CI
|
||||||
minioIngress:
|
minioIngress:
|
||||||
|
|||||||
@@ -136,6 +136,25 @@ postgresql:
|
|||||||
persistence:
|
persistence:
|
||||||
enabled: false
|
enabled: false
|
||||||
size: 10Gi
|
size: 10Gi
|
||||||
|
# Resources with memory requests = limits per cluster policy
|
||||||
|
resourcesPreset: "none"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
# Volume permissions init container
|
||||||
|
volumePermissions:
|
||||||
|
resourcesPreset: "none"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
|
||||||
# MinIO subchart configuration
|
# MinIO subchart configuration
|
||||||
minio:
|
minio:
|
||||||
@@ -152,6 +171,35 @@ minio:
|
|||||||
persistence:
|
persistence:
|
||||||
enabled: false
|
enabled: false
|
||||||
size: 50Gi
|
size: 50Gi
|
||||||
|
# Resources with memory requests = limits per cluster policy
|
||||||
|
resourcesPreset: "none" # Disable preset to use explicit resources
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
# Init container resources
|
||||||
|
defaultInitContainers:
|
||||||
|
volumePermissions:
|
||||||
|
resourcesPreset: "none"
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
# Provisioning job resources
|
||||||
|
provisioning:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
|
||||||
# MinIO external ingress for presigned URL access (separate from subchart ingress)
|
# MinIO external ingress for presigned URL access (separate from subchart ingress)
|
||||||
minioIngress:
|
minioIngress:
|
||||||
|
|||||||
26
kics.config
26
kics.config
@@ -23,3 +23,29 @@ exclude-queries:
|
|||||||
# Reason: We intentionally don't pin curl version to get security updates.
|
# Reason: We intentionally don't pin curl version to get security updates.
|
||||||
# This is documented with hadolint ignore comment in Dockerfile.
|
# This is documented with hadolint ignore comment in Dockerfile.
|
||||||
- 965a08d7-ef86-4f14-8792-4a3b2098937e
|
- 965a08d7-ef86-4f14-8792-4a3b2098937e
|
||||||
|
|
||||||
|
# Container Capabilities Unrestricted (MEDIUM)
|
||||||
|
# Reason: LOCAL DEVELOPMENT ONLY. Stock postgres, redis, minio images require
|
||||||
|
# certain capabilities (SETUID, SETGID, CHOWN) to switch users at startup.
|
||||||
|
# cap_drop: ALL breaks these containers. Production Kubernetes deployments
|
||||||
|
# use securityContext with appropriate settings.
|
||||||
|
- ce76b7d0-9e77-464d-b86f-c5c48e03e22d
|
||||||
|
|
||||||
|
# No New Privileges Not Set (HIGH)
|
||||||
|
# Reason: LOCAL DEVELOPMENT ONLY. Stock postgres, redis, minio images need
|
||||||
|
# to escalate privileges during initialization (e.g., postgres switches from
|
||||||
|
# root to postgres user). no-new-privileges:true prevents this and causes
|
||||||
|
# containers to crash. Production Kubernetes deployments handle this via
|
||||||
|
# securityContext.
|
||||||
|
- 27fcc7d6-c49b-46e0-98f1-6c082a6a2750
|
||||||
|
|
||||||
|
# Security Opt Not Set (MEDIUM)
|
||||||
|
# Reason: LOCAL DEVELOPMENT ONLY. Related to above - security_opt is not set
|
||||||
|
# on database services because no-new-privileges breaks them.
|
||||||
|
- 610e266e-6c12-4bca-9925-1ed0cd29742b
|
||||||
|
|
||||||
|
# Container Traffic Not Bound To Host Interface (MEDIUM)
|
||||||
|
# Reason: LOCAL DEVELOPMENT ONLY. The orchard-server port is bound to 0.0.0.0
|
||||||
|
# to allow testing from other machines on the local network. This is only in
|
||||||
|
# docker-compose.local.yml, not production deployments.
|
||||||
|
- 451d79dc-0588-476a-ad03-3c7f0320abb3
|
||||||
|
|||||||
Reference in New Issue
Block a user