general fixes
This commit is contained in:
@@ -41,6 +41,14 @@ COOKIE_DOMAIN=
|
|||||||
# Cookie max age in seconds (default: 3600 = 1 hour)
|
# Cookie max age in seconds (default: 3600 = 1 hour)
|
||||||
COOKIE_MAX_AGE=3600
|
COOKIE_MAX_AGE=3600
|
||||||
|
|
||||||
|
# -------------------------------------------
|
||||||
|
# Reverse Proxy / TLS Termination
|
||||||
|
# -------------------------------------------
|
||||||
|
# Enable to trust X-Forwarded-* headers (set true behind ingress/nginx)
|
||||||
|
PROXY_HEADERS=false
|
||||||
|
# Comma-separated trusted proxy hosts/IPs (use "*" to trust all)
|
||||||
|
TRUSTED_PROXY_HOSTS=127.0.0.1,::1
|
||||||
|
|
||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
# Database Configuration (PostgreSQL)
|
# Database Configuration (PostgreSQL)
|
||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ class Settings(BaseSettings):
|
|||||||
cookie_max_age: int = Field(default=28800, env="COOKIE_MAX_AGE") # 8 hours
|
cookie_max_age: int = Field(default=28800, env="COOKIE_MAX_AGE") # 8 hours
|
||||||
cookie_name: str = Field(default="access_token", env="COOKIE_NAME")
|
cookie_name: str = Field(default="access_token", env="COOKIE_NAME")
|
||||||
|
|
||||||
|
# Proxy / TLS termination settings
|
||||||
|
# Enable to honor X-Forwarded-Proto when behind a reverse proxy (ingress/nginx)
|
||||||
|
proxy_headers: bool = Field(default=False, env="PROXY_HEADERS")
|
||||||
|
# Comma-separated list of trusted proxy hosts/IPs. Use "*" to trust all.
|
||||||
|
trusted_proxy_hosts: str = Field(default="127.0.0.1,::1", env="TRUSTED_PROXY_HOSTS")
|
||||||
|
|
||||||
# Database settings
|
# Database settings
|
||||||
database_host: str = Field(default="postgres", env="DATABASE_HOST")
|
database_host: str = Field(default="postgres", env="DATABASE_HOST")
|
||||||
database_port: int = Field(default=5432, env="DATABASE_PORT")
|
database_port: int = Field(default=5432, env="DATABASE_PORT")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from fastapi import FastAPI, Depends, Request, HTTPException, status
|
from fastapi import FastAPI, Depends, Request, HTTPException, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -139,6 +140,15 @@ app = FastAPI(
|
|||||||
lifespan=lifespan
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Respect X-Forwarded-Proto/For headers when behind a reverse proxy
|
||||||
|
if settings.proxy_headers:
|
||||||
|
trusted_hosts = [
|
||||||
|
host.strip()
|
||||||
|
for host in settings.trusted_proxy_hosts.split(",")
|
||||||
|
if host.strip()
|
||||||
|
]
|
||||||
|
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=trusted_hosts)
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ interface LanguageSelectorProps {
|
|||||||
export default function LanguageSelector({ className = '', compact = false }: LanguageSelectorProps) {
|
export default function LanguageSelector({ className = '', compact = false }: LanguageSelectorProps) {
|
||||||
const { i18n, t } = useTranslation('common')
|
const { i18n, t } = useTranslation('common')
|
||||||
|
|
||||||
|
const resolvedLanguage = i18n.resolvedLanguage || i18n.language
|
||||||
|
const normalizedLanguage = (resolvedLanguage?.split('-')[0] || 'en') as LanguageCode
|
||||||
|
const selectedLanguage = LANGUAGES.some((lang) => lang.code === normalizedLanguage)
|
||||||
|
? normalizedLanguage
|
||||||
|
: 'en'
|
||||||
|
|
||||||
const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const newLang = e.target.value as LanguageCode
|
const newLang = e.target.value as LanguageCode
|
||||||
i18n.changeLanguage(newLang)
|
i18n.changeLanguage(newLang)
|
||||||
@@ -23,7 +29,7 @@ export default function LanguageSelector({ className = '', compact = false }: La
|
|||||||
return (
|
return (
|
||||||
<div className={`relative ${className}`}>
|
<div className={`relative ${className}`}>
|
||||||
<select
|
<select
|
||||||
value={i18n.language}
|
value={selectedLanguage}
|
||||||
onChange={handleLanguageChange}
|
onChange={handleLanguageChange}
|
||||||
className="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-1.5 text-sm text-gray-700 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent cursor-pointer"
|
className="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-1.5 text-sm text-gray-700 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent cursor-pointer"
|
||||||
title={t('selectLanguage')}
|
title={t('selectLanguage')}
|
||||||
|
|||||||
@@ -24,9 +24,8 @@
|
|||||||
},
|
},
|
||||||
"noProjectWarning": {
|
"noProjectWarning": {
|
||||||
"title": "No Project Selected",
|
"title": "No Project Selected",
|
||||||
"messagePart1": "Please select a project from the dropdown above or",
|
"messagePart1": "Please select a project from the dropdown above.",
|
||||||
"createProject": "create a new project",
|
"messagePart2": "New projects can be created in the Super Admin panel."
|
||||||
"messagePart2": "to get started."
|
|
||||||
},
|
},
|
||||||
"quickFilters": {
|
"quickFilters": {
|
||||||
"title": "Quick Search Filters",
|
"title": "Quick Search Filters",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"relationships": {
|
"relationships": {
|
||||||
"title": "Relationships",
|
"title": "Relationships",
|
||||||
"addButton": "Add Relationship",
|
"addButton": "Add Relationship",
|
||||||
|
"manageTypesButton": "Manage Relationship Types",
|
||||||
"noTypesWarning": "No relationship types have been defined for this project. Contact an administrator to set up relationship types.",
|
"noTypesWarning": "No relationship types have been defined for this project. Contact an administrator to set up relationship types.",
|
||||||
"loadingRelationships": "Loading relationships...",
|
"loadingRelationships": "Loading relationships...",
|
||||||
"noRelationships": "No relationships defined yet.",
|
"noRelationships": "No relationships defined yet.",
|
||||||
|
|||||||
@@ -24,9 +24,8 @@
|
|||||||
},
|
},
|
||||||
"noProjectWarning": {
|
"noProjectWarning": {
|
||||||
"title": "Nenhum Projeto Selecionado",
|
"title": "Nenhum Projeto Selecionado",
|
||||||
"messagePart1": "Por favor, selecione um projeto no menu acima ou",
|
"messagePart1": "Por favor, selecione um projeto no menu acima.",
|
||||||
"createProject": "crie um novo projeto",
|
"messagePart2": "Novos projetos podem ser criados no painel de Super Admin."
|
||||||
"messagePart2": "para começar."
|
|
||||||
},
|
},
|
||||||
"quickFilters": {
|
"quickFilters": {
|
||||||
"title": "Filtros Rápidos de Busca",
|
"title": "Filtros Rápidos de Busca",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"relationships": {
|
"relationships": {
|
||||||
"title": "Relacionamentos",
|
"title": "Relacionamentos",
|
||||||
"addButton": "Adicionar Relacionamento",
|
"addButton": "Adicionar Relacionamento",
|
||||||
|
"manageTypesButton": "Gerenciar Tipos de Relacionamento",
|
||||||
"noTypesWarning": "Nenhum tipo de relacionamento foi definido para este projeto. Contate um administrador para configurar os tipos de relacionamento.",
|
"noTypesWarning": "Nenhum tipo de relacionamento foi definido para este projeto. Contate um administrador para configurar os tipos de relacionamento.",
|
||||||
"loadingRelationships": "Carregando relacionamentos...",
|
"loadingRelationships": "Carregando relacionamentos...",
|
||||||
"noRelationships": "Nenhum relacionamento definido ainda.",
|
"noRelationships": "Nenhum relacionamento definido ainda.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth, useProject } from '@/hooks'
|
import { useAuth, useProject } from '@/hooks'
|
||||||
import {
|
import {
|
||||||
@@ -19,10 +19,22 @@ export default function AdminPage() {
|
|||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const { currentProject, setCurrentProject } = useProject()
|
const { currentProject, setCurrentProject } = useProject()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
// Tab state
|
// Tab state
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('project')
|
const [activeTab, setActiveTab] = useState<TabType>('project')
|
||||||
|
|
||||||
|
// Allow navigation to a specific tab (e.g., relationships)
|
||||||
|
useEffect(() => {
|
||||||
|
const tabFromState = (location.state as { tab?: TabType } | null)?.tab
|
||||||
|
const tabFromQuery = new URLSearchParams(location.search).get('tab') as TabType | null
|
||||||
|
const nextTab = tabFromState || tabFromQuery
|
||||||
|
|
||||||
|
if (nextTab && ['project', 'members', 'relationships'].includes(nextTab) && nextTab !== activeTab) {
|
||||||
|
setActiveTab(nextTab)
|
||||||
|
}
|
||||||
|
}, [location.state, location.search, activeTab])
|
||||||
|
|
||||||
// Project settings state
|
// Project settings state
|
||||||
const [projectName, setProjectName] = useState('')
|
const [projectName, setProjectName] = useState('')
|
||||||
const [projectDesc, setProjectDesc] = useState('')
|
const [projectDesc, setProjectDesc] = useState('')
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ interface GroupStats {
|
|||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading, createProject } = useProject()
|
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading } = useProject()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation('dashboard')
|
const { t } = useTranslation('dashboard')
|
||||||
const { t: tCommon } = useTranslation('common')
|
const { t: tCommon } = useTranslation('common')
|
||||||
@@ -88,11 +88,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
// Project dropdown state
|
// Project dropdown state
|
||||||
const [showProjectDropdown, setShowProjectDropdown] = useState(false)
|
const [showProjectDropdown, setShowProjectDropdown] = useState(false)
|
||||||
const [showCreateProjectModal, setShowCreateProjectModal] = useState(false)
|
|
||||||
const [newProjectName, setNewProjectName] = useState('')
|
|
||||||
const [newProjectDesc, setNewProjectDesc] = useState('')
|
|
||||||
const [createProjectLoading, setCreateProjectLoading] = useState(false)
|
|
||||||
const [createProjectError, setCreateProjectError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// Calculate stats for each group
|
// Calculate stats for each group
|
||||||
const getGroupStats = (groupId: number): GroupStats => {
|
const getGroupStats = (groupId: number): GroupStats => {
|
||||||
@@ -212,38 +208,6 @@ export default function DashboardPage() {
|
|||||||
setShowProjectDropdown(false)
|
setShowProjectDropdown(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateProject = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (!newProjectName.trim()) {
|
|
||||||
setCreateProjectError('Project name is required')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setCreateProjectLoading(true)
|
|
||||||
setCreateProjectError(null)
|
|
||||||
|
|
||||||
const newProject = await createProject(
|
|
||||||
newProjectName.trim(),
|
|
||||||
newProjectDesc.trim() || undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
// Select the newly created project
|
|
||||||
setCurrentProject(newProject)
|
|
||||||
|
|
||||||
// Close modal and reset form
|
|
||||||
setShowCreateProjectModal(false)
|
|
||||||
setNewProjectName('')
|
|
||||||
setNewProjectDesc('')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to create project:', err)
|
|
||||||
setCreateProjectError('Failed to create project. Please try again.')
|
|
||||||
} finally {
|
|
||||||
setCreateProjectLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
<div className="min-h-screen bg-gray-50 flex">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
@@ -373,16 +337,6 @@ export default function DashboardPage() {
|
|||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
<hr className="my-1 border-gray-200" />
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowProjectDropdown(false)
|
|
||||||
setShowCreateProjectModal(true)
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-4 py-2 text-sm text-teal-600 hover:bg-gray-100 font-medium"
|
|
||||||
>
|
|
||||||
{t('projectDropdown.createNewProject')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -473,14 +427,7 @@ export default function DashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-amber-800">{t('noProjectWarning.title')}</h3>
|
<h3 className="font-semibold text-amber-800">{t('noProjectWarning.title')}</h3>
|
||||||
<p className="text-sm text-amber-700">
|
<p className="text-sm text-amber-700">
|
||||||
{t('noProjectWarning.messagePart1')}{' '}
|
{t('noProjectWarning.messagePart1')} {t('noProjectWarning.messagePart2')}
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateProjectModal(true)}
|
|
||||||
className="underline font-medium hover:text-amber-900"
|
|
||||||
>
|
|
||||||
{t('noProjectWarning.createProject')}
|
|
||||||
</button>
|
|
||||||
{' '}{t('noProjectWarning.messagePart2')}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -790,95 +737,6 @@ export default function DashboardPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create Project Modal */}
|
|
||||||
{showCreateProjectModal && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
||||||
{/* Modal Header */}
|
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-800">{t('createProject.title')}</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowCreateProjectModal(false)
|
|
||||||
setCreateProjectError(null)
|
|
||||||
setNewProjectName('')
|
|
||||||
setNewProjectDesc('')
|
|
||||||
}}
|
|
||||||
className="text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal Body */}
|
|
||||||
<form onSubmit={handleCreateProject}>
|
|
||||||
<div className="px-6 py-4 space-y-4">
|
|
||||||
{/* Error message */}
|
|
||||||
{createProjectError && (
|
|
||||||
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
|
||||||
{createProjectError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Project Name */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
{t('createProject.projectName')} <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newProjectName}
|
|
||||||
onChange={(e) => setNewProjectName(e.target.value)}
|
|
||||||
placeholder={t('createProject.projectNamePlaceholder')}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Project Description */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
{t('createProject.description')}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={newProjectDesc}
|
|
||||||
onChange={(e) => setNewProjectDesc(e.target.value)}
|
|
||||||
placeholder={t('createProject.descriptionPlaceholder')}
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal Footer */}
|
|
||||||
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShowCreateProjectModal(false)
|
|
||||||
setCreateProjectError(null)
|
|
||||||
setNewProjectName('')
|
|
||||||
setNewProjectDesc('')
|
|
||||||
}}
|
|
||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
|
||||||
disabled={createProjectLoading}
|
|
||||||
>
|
|
||||||
{tCommon('cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
disabled={createProjectLoading}
|
|
||||||
>
|
|
||||||
{createProjectLoading ? t('createProject.creating') : t('createProject.createButton')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -718,6 +718,7 @@ export default function RequirementDetailPage() {
|
|||||||
|
|
||||||
// Check if requirement is in draft status
|
// Check if requirement is in draft status
|
||||||
const isDraftStatus = requirement?.status?.status_code === 'DRAFT'
|
const isDraftStatus = requirement?.status?.status_code === 'DRAFT'
|
||||||
|
const isAdmin = user?.role_id === 3
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -852,16 +853,26 @@ export default function RequirementDetailPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-xl font-bold text-gray-800">{t('relationships.title')}</h3>
|
<h3 className="text-xl font-bold text-gray-800">{t('relationships.title')}</h3>
|
||||||
{!isAuditor && (
|
<div className="flex items-center gap-2">
|
||||||
<button
|
{isAdmin && (
|
||||||
onClick={openAddRelationshipModal}
|
<button
|
||||||
disabled={relationshipTypes.length === 0}
|
onClick={() => navigate('/admin', { state: { tab: 'relationships' } })}
|
||||||
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||||
title={relationshipTypes.length === 0 ? t('relationships.noTypesWarning') : ''}
|
>
|
||||||
>
|
{t('relationships.manageTypesButton')}
|
||||||
{t('relationships.addButton')}
|
</button>
|
||||||
</button>
|
)}
|
||||||
)}
|
{!isAuditor && (
|
||||||
|
<button
|
||||||
|
onClick={openAddRelationshipModal}
|
||||||
|
disabled={relationshipTypes.length === 0}
|
||||||
|
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title={relationshipTypes.length === 0 ? t('relationships.noTypesWarning') : ''}
|
||||||
|
>
|
||||||
|
{t('relationships.addButton')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{relationshipTypes.length === 0 && !relationshipsLoading && (
|
{relationshipTypes.length === 0 && !relationshipsLoading && (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ metadata:
|
|||||||
name: fastapi-app
|
name: fastapi-app
|
||||||
namespace: requirements-periodic-table
|
namespace: requirements-periodic-table
|
||||||
spec:
|
spec:
|
||||||
replicas: 2
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: fastapi-app
|
app: fastapi-app
|
||||||
@@ -18,20 +18,20 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: fastapi-app
|
app: fastapi-app
|
||||||
spec:
|
spec:
|
||||||
imagePullSecrets:
|
|
||||||
- name: regcred
|
|
||||||
containers:
|
containers:
|
||||||
- name: fastapi-app
|
- name: fastapi-app
|
||||||
image: docker.io/your-dockerhub-username/periodic-table-backend:latest
|
image: docker.io/gulimabr/requirements-periodic-table-backend:latest
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
env:
|
|
||||||
- name: DATABASE_HOST
|
|
||||||
value: postgresql
|
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- configMapRef:
|
||||||
name: periodic-table-env # Provide secrets for APP settings
|
name: periodic-table-env # Provide config for APP settings
|
||||||
|
env:
|
||||||
|
- name: PROXY_HEADERS
|
||||||
|
value: "true"
|
||||||
|
- name: TRUSTED_PROXY_HOSTS
|
||||||
|
value: "*"
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
@@ -53,7 +53,7 @@ metadata:
|
|||||||
name: frontend
|
name: frontend
|
||||||
namespace: requirements-periodic-table
|
namespace: requirements-periodic-table
|
||||||
spec:
|
spec:
|
||||||
replicas: 2
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: frontend
|
app: frontend
|
||||||
@@ -62,12 +62,10 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: frontend
|
app: frontend
|
||||||
spec:
|
spec:
|
||||||
imagePullSecrets:
|
|
||||||
- name: regcred
|
|
||||||
containers:
|
containers:
|
||||||
- name: frontend
|
- name: frontend
|
||||||
image: docker.io/your-dockerhub-username/periodic-table-frontend:latest
|
image: docker.io/gulimabr/requirements-periodic-table-frontend:latest
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user