project & marketplace via factory

This commit is contained in:
typescreep
2025-06-06 15:36:14 +03:00
parent 70a0ac96a4
commit c469a580f7
55 changed files with 59 additions and 1366 deletions

3
.env
View File

@@ -1,4 +1,7 @@
VITE_CUSTOMIZATION_API_GROUP=incloud.io
VITE_CUSTOMIZATION_API_VERSION=v1alpha
VITE_PROJECTS_VERSION=v1alpha
VITE_PROJECTS_RESOURCE_NAME=projects
VITE_MARKETPLACE_RESOURCE_NAME=marketplacepanels
VITE_MARKETPLACE_KIND=MarketplacePanel
VITE_INSTANCES_VERSION=v1alpha1

8
package-lock.json generated
View File

@@ -11,7 +11,7 @@
"@ant-design/icons": "5.6.0",
"@monaco-editor/react": "4.6.0",
"@originjs/vite-plugin-federation": "1.3.6",
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.22",
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.23",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",
@@ -2752,9 +2752,9 @@
}
},
"node_modules/@prorobotech/openapi-k8s-toolkit": {
"version": "0.0.1-alpha.22",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.22.tgz",
"integrity": "sha512-907Jubl1g7CcSK/YMakSTgbz/+FF4mDnL5hT2khQAUusW/7sPD0B8f2VEYa8Ace511q/XuLNo7STUDfmXEEcMQ==",
"version": "0.0.1-alpha.23",
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.23.tgz",
"integrity": "sha512-3lxhPTGbqca3emK5zkQRUuiNc424TpotmExno7YFMN66DpXDT42MsCQQ0VF7iPbVPwVH6bWmOMtqe6M5hP6j3w==",
"license": "MIT",
"dependencies": {
"@monaco-editor/react": "4.6.0",

View File

@@ -17,7 +17,7 @@
"@ant-design/icons": "5.6.0",
"@monaco-editor/react": "4.6.0",
"@originjs/vite-plugin-federation": "1.3.6",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.22",
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.23",
"@readme/openapi-parser": "4.0.0",
"@reduxjs/toolkit": "2.2.5",
"@tanstack/react-query": "5.62.2",

View File

@@ -127,7 +127,10 @@ export const App: FC<TAppProps> = ({ isFederation, forcedTheme }) => {
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/forms/crds/:apiGroup/:apiVersion/:typeName/:entryName?/`}
element={<FormCrdPage forcedTheme={forcedTheme} />}
/>
<Route path={`${prefix}/:clusterName/factory/:key/*`} element={<FactoryPage forcedTheme={forcedTheme} />} />
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/factory/:key/*`}
element={<FactoryPage forcedTheme={forcedTheme} />}
/>
<Route path={`${prefix}/factory-admin/*`} element={<FactoryAdminPage />} />
</Routes>
)

View File

@@ -1,25 +0,0 @@
import React, { FC, ReactNode } from 'react'
import { theme } from 'antd'
import { Styled } from './styled'
type TContentCardProps = {
children?: ReactNode
flexGrow?: number
displayFlex?: boolean
flexFlow?: string
}
export const ContentCard: FC<TContentCardProps> = ({ children, flexGrow, displayFlex, flexFlow }) => {
const { token } = theme.useToken()
return (
<Styled.ContentContainer
$flexGrow={flexGrow}
$bgColor={token.colorBgContainer}
$borderColor={token.colorBorder}
$displayFlex={displayFlex}
$flexFlow={flexFlow}
>
{children}
</Styled.ContentContainer>
)
}

View File

@@ -1 +0,0 @@
export * from './ContentCard'

View File

@@ -1,25 +0,0 @@
import styled from 'styled-components'
type TContentContainerProps = {
$bgColor: string
$borderColor: string
$flexGrow?: number
$displayFlex?: boolean
$flexFlow?: string
}
export const ContentContainer = styled.div<TContentContainerProps>`
border: 1px solid ${({ $borderColor }) => $borderColor};
border-radius: 6px;
background-color: ${({ $bgColor }) => $bgColor};
width: 100%;
height: 100%;
padding: 24px;
flex-grow: ${({ $flexGrow }) => $flexGrow};
display: ${({ $displayFlex }) => ($displayFlex ? 'flex' : 'block')};
flex-flow: ${({ $flexFlow }) => $flexFlow};
`
export const Styled = {
ContentContainer,
}

View File

@@ -1,15 +0,0 @@
import React, { FC } from 'react'
import { theme } from 'antd'
export const DeleteIcon: FC = () => {
const { token } = theme.useToken()
return (
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.25 3.5H14.25V1.625C14.25 0.797656 13.5773 0.125 12.75 0.125H5.25C4.42266 0.125 3.75 0.797656 3.75 1.625V3.5H0.75C0.335156 3.5 0 3.83516 0 4.25V5C0 5.10313 0.084375 5.1875 0.1875 5.1875H1.60312L2.18203 17.4453C2.21953 18.2445 2.88047 18.875 3.67969 18.875H14.3203C15.1219 18.875 15.7805 18.2469 15.818 17.4453L16.3969 5.1875H17.8125C17.9156 5.1875 18 5.10313 18 5V4.25C18 3.83516 17.6648 3.5 17.25 3.5ZM12.5625 3.5H5.4375V1.8125H12.5625V3.5Z"
fill={token.colorText}
/>
</svg>
)
}

View File

@@ -1,15 +0,0 @@
import React, { FC } from 'react'
import { theme } from 'antd'
export const EditIcon: FC = () => {
const { token } = theme.useToken()
return (
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.28 16.79H0.72C0.32175 16.79 0 17.1117 0 17.51V18.32C0 18.419 0.081 18.5 0.18 18.5H17.82C17.919 18.5 18 18.419 18 18.32V17.51C18 17.1117 17.6783 16.79 17.28 16.79ZM3.27825 14.9C3.32325 14.9 3.36825 14.8955 3.41325 14.8888L7.19775 14.225C7.24275 14.216 7.2855 14.1958 7.317 14.162L16.8547 4.62425C16.8756 4.60343 16.8922 4.57871 16.9034 4.55149C16.9147 4.52427 16.9205 4.49509 16.9205 4.46562C16.9205 4.43616 16.9147 4.40698 16.9034 4.37976C16.8922 4.35254 16.8756 4.32782 16.8547 4.307L13.1153 0.56525C13.0725 0.5225 13.0163 0.5 12.9555 0.5C12.8948 0.5 12.8385 0.5225 12.7958 0.56525L3.258 10.103C3.22425 10.1367 3.204 10.1772 3.195 10.2222L2.53125 14.0067C2.50936 14.1273 2.51718 14.2513 2.55404 14.3682C2.59089 14.485 2.65566 14.5911 2.74275 14.6772C2.89125 14.8212 3.078 14.9 3.27825 14.9Z"
fill={token.colorText}
/>
</svg>
)
}

View File

@@ -1,2 +0,0 @@
export * from './DeleteIcon'
export * from './EditIcon'

View File

@@ -0,0 +1,18 @@
import { FC, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
export const RedirectProjectInfo: FC = () => {
const { clusterName, namespace } = useParams()
const navigate = useNavigate()
const baseprefix = useSelector((state: RootState) => state.baseprefix.baseprefix)
navigate(`${baseprefix}/${clusterName}/${namespace}/factory/project/${namespace}`)
useEffect(() => {
navigate(`${baseprefix}/${clusterName}/${namespace}/factory/project/${namespace}`)
}, [clusterName, namespace, baseprefix, navigate])
return null
}

View File

@@ -0,0 +1 @@
export * from './RedirectProjectInfo'

View File

@@ -4,6 +4,5 @@ export * from './TitleWithNoTopMargin'
export * from './ThemeSelector'
export * from './FlexEnd'
export * from './BackLink'
export * from './ContentCard'
export * from './FlexGrow'
export * from './Icons'
export * from './RedirectProjectInfo'

View File

@@ -1,241 +0,0 @@
/* eslint-disable max-lines-per-function */
import React, { FC, useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { AxiosError } from 'axios'
import { usePermissions, useDirectUnknownResource, DeleteModal, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { notification, Typography, Flex, Switch, Spin, Alert } from 'antd'
import { BASE_API_GROUP, BASE_API_VERSION } from 'constants/customizationApiGroupAndVersion'
import { TMarketPlacePanelResponse, TMarketPlacePanelResource, TMarketPlacePanel } from 'localTypes/marketplace'
import { AddCard } from './atoms'
import { AddEditFormModal, MarketplaceCard, SearchTextInput } from './molecules'
import { Styled } from './styled'
export const MarketPlace: FC = () => {
const [api, contextHolder] = notification.useNotification()
const [isEditMode, setIsEditMode] = useState<boolean>(false)
const [isAddEditOpen, setIsAddEditOpen] = useState<boolean | TMarketPlacePanelResource>(false)
const [isDeleteOpen, setIsDeleteOpen] = useState<{ name: string } | boolean>(false)
const [createUpdateError, setCreateUpdateError] = useState<AxiosError | Error>()
const [deleteError, setDeleteError] = useState<AxiosError | Error>()
const [initialData, setInitialData] = useState<TMarketPlacePanel[]>([])
const [filteredAndSortedData, setFilterAndSortedData] = useState<TMarketPlacePanel[]>([])
const [uniqueTags, setUniqueTags] = useState<string[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>([])
const { clusterName, namespace } = useParams()
const {
data: marketplacePanels,
isLoading,
error,
} = useDirectUnknownResource<TMarketPlacePanelResponse>({
uri: `/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/${BASE_API_VERSION}/marketplacepanels/`,
refetchInterval: 5000,
queryKey: ['marketplacePanels', clusterName || 'no-cluster'],
isEnabled: clusterName !== undefined,
})
const createPermission = usePermissions({
apiGroup: BASE_API_GROUP,
typeName: 'marketplacepanels',
namespace: '',
clusterName: clusterName || '',
verb: 'create',
refetchInterval: false,
})
const updatePermission = usePermissions({
apiGroup: BASE_API_GROUP,
typeName: 'marketplacepanels',
namespace: '',
clusterName: clusterName || '',
verb: 'update',
refetchInterval: false,
})
const deletePermission = usePermissions({
apiGroup: BASE_API_GROUP,
typeName: 'marketplacepanels',
namespace: '',
clusterName: clusterName || '',
verb: 'delete',
refetchInterval: false,
})
const onCreateSuccess = () =>
api.success({
message: 'Card created',
key: 'create-marketplace-success',
})
const onUpdateSuccess = () =>
api.success({
message: 'Card modified',
key: 'update-marketplace-success',
})
useEffect(() => {
if (marketplacePanels) {
if (isEditMode) {
setInitialData(marketplacePanels.items.map(({ spec }) => spec).sort())
setUniqueTags(
marketplacePanels.items
.flatMap(({ spec }) => spec.tags)
.filter((value, index, arr) => arr.indexOf(value) === index),
)
} else {
setInitialData(
marketplacePanels.items
.map(({ spec }) => spec)
.filter(({ hidden }) => hidden !== true)
.sort(),
)
setUniqueTags(
marketplacePanels.items
.filter(({ spec }) => spec.hidden !== true)
.flatMap(({ spec }) => spec.tags)
.filter((value, index, arr) => arr.indexOf(value) === index),
)
}
}
}, [marketplacePanels, isEditMode])
useEffect(() => {
let newData: TMarketPlacePanel[] = []
if (selectedTags && selectedTags.length > 0) {
newData = initialData
.filter(
({ name, description, tags }) =>
selectedTags.some(tag => name.toLowerCase().includes(tag.toLowerCase())) ||
selectedTags.some(tag => description.toLowerCase().includes(tag.toLowerCase())) ||
selectedTags.some(tag => tags.some(el => el.toLowerCase().includes(tag.toLowerCase()))),
)
.sort()
} else {
newData = initialData.sort()
}
setFilterAndSortedData(newData)
}, [selectedTags, initialData])
if (error) {
return <div>{JSON.stringify(error)}</div>
}
if (isLoading) {
return <Spin />
}
if (!marketplacePanels) {
return <div>No panels</div>
}
const toggleEditMode = () => {
setIsEditMode(!isEditMode)
}
return (
<>
{contextHolder}
<Flex justify="space-between">
<div>
<Flex gap={12} vertical>
<div>
<Typography.Text type="secondary">Available Products</Typography.Text>
</div>
<div>
<Styled.BigValue>Marketplace</Styled.BigValue>
</div>
</Flex>
</div>
<div>
<Flex gap={12} vertical>
<SearchTextInput uniqueTags={uniqueTags} selectedTags={selectedTags} onSelectedTags={setSelectedTags} />
{(createPermission.data?.status.allowed ||
updatePermission.data?.status.allowed ||
deletePermission.data?.status.allowed) && (
<Flex align="center" gap={8}>
<Switch defaultChecked checked={isEditMode} onClick={toggleEditMode} /> Edit Mode
</Flex>
)}
</Flex>
</div>
</Flex>
<Spacer $space={20} $samespace />
{createUpdateError && (
<Alert
description={JSON.stringify(createUpdateError)}
message="Card was not created"
onClose={() => setCreateUpdateError(undefined)}
type="error"
/>
)}
{deleteError && (
<Alert
description={JSON.stringify(deleteError)}
message="Card was not deleted"
onClose={() => setDeleteError(undefined)}
type="error"
/>
)}
<Flex gap={22} wrap>
{clusterName &&
namespace &&
filteredAndSortedData.map(
({ name, description, icon, type, pathToNav, typeName, apiGroup, apiVersion, tags, disabled }) => (
<MarketplaceCard
key={name}
description={description}
disabled={disabled}
icon={icon}
isEditMode={isEditMode}
name={name}
clusterName={clusterName}
namespace={namespace}
type={type}
pathToNav={pathToNav}
typeName={typeName}
apiGroup={apiGroup}
apiVersion={apiVersion}
tags={tags}
onDeleteClick={() => {
const entry = marketplacePanels.items.find(({ spec }) => spec.name === name)
setIsDeleteOpen(entry ? { name: entry.metadata.name } : false)
}}
onEditClick={() => {
setIsAddEditOpen(marketplacePanels.items.find(({ spec }) => spec.name === name) || false)
}}
/>
),
)}
{isEditMode && (
<AddCard
onAddClick={() => {
setIsAddEditOpen(true)
}}
/>
)}
</Flex>
{isAddEditOpen && (
<AddEditFormModal
isOpen={isAddEditOpen}
setError={setCreateUpdateError}
setIsOpen={setIsAddEditOpen}
onCreateSuccess={onCreateSuccess}
onUpdateSuccess={onUpdateSuccess}
/>
)}
{typeof isDeleteOpen !== 'boolean' && (
<DeleteModal
name={isDeleteOpen.name}
onClose={() => setIsDeleteOpen(false)}
endpoint={`/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/${BASE_API_VERSION}/marketplacepanels/${isDeleteOpen.name}`}
/>
)}
</>
)
}

View File

@@ -1,15 +0,0 @@
import React, { FC } from 'react'
import { PlusOutlined } from '@ant-design/icons'
import { Styled } from './styled'
type TAddCardProps = {
onAddClick: () => void
}
export const AddCard: FC<TAddCardProps> = ({ onAddClick }) => {
return (
<Styled.CustomCard onClick={onAddClick}>
<PlusOutlined />
</Styled.CustomCard>
)
}

View File

@@ -1 +0,0 @@
export * from './AddCard'

View File

@@ -1,27 +0,0 @@
import styled from 'styled-components'
import { Card } from 'antd'
const CustomCard = styled(Card)`
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
width: 238px;
box-shadow:
0 6px 16px 0 #00000014,
0 3px 6px -4px #0000001f,
0 9px 28px 8px #0000000d;
.ant-card-body {
display: flex;
justify-content: center;
align-items: center;
height: 238px;
overflow-x: auto;
padding: 8px;
}
`
export const Styled = {
CustomCard,
}

View File

@@ -1 +0,0 @@
export * from './AddCard'

View File

@@ -1,2 +0,0 @@
export * from './MarketPlace'
export * from './molecules/MarketplaceCard'

View File

@@ -1,164 +0,0 @@
import React, { FC, Dispatch, SetStateAction } from 'react'
import { isAxiosError, AxiosError } from 'axios'
import { Form, Input, Select, Switch, Modal } from 'antd'
import { createNewEntry, updateEntry } from '@prorobotech/openapi-k8s-toolkit'
import { useParams } from 'react-router-dom'
import { v4 as uuidv4 } from 'uuid'
import { BASE_API_GROUP, BASE_API_VERSION } from 'constants/customizationApiGroupAndVersion'
import { TMarketPlacePanel, TMarketPlacePanelResource } from 'localTypes/marketplace'
type TAddEditFormModalProps = {
isOpen: boolean | TMarketPlacePanelResource
setIsOpen: Dispatch<SetStateAction<boolean | TMarketPlacePanelResource>>
setError: Dispatch<SetStateAction<AxiosError | Error | undefined>>
onCreateSuccess: () => void
onUpdateSuccess: () => void
}
export const AddEditFormModal: FC<TAddEditFormModalProps> = ({
isOpen,
setIsOpen,
setError,
onCreateSuccess,
onUpdateSuccess,
}) => {
const [form] = Form.useForm<TMarketPlacePanel>()
const type = Form.useWatch<string | undefined>('type', form)
const { clusterName } = useParams()
const defaultValues: TMarketPlacePanel =
typeof isOpen === 'boolean'
? {
name: '',
description: '',
icon: '',
type: 'direct',
apiGroup: '',
apiVersion: '',
typeName: '',
pathToNav: '',
tags: [],
disabled: false,
hidden: false,
}
: isOpen.spec
const onSubmit = (values: TMarketPlacePanel) => {
if (typeof isOpen === 'boolean') {
createNewEntry({
endpoint: `/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/{BASE_API_VERSION}/marketplacepanels`,
body: {
apiVersion: `${BASE_API_GROUP}/${BASE_API_VERSION}`,
kind: 'MarketplacePanel',
metadata: {
name: uuidv4(),
},
spec: { ...values },
},
})
.then(() => {
setIsOpen(false)
onCreateSuccess()
})
.catch(err => {
if (isAxiosError(err) || err instanceof Error) {
setError(err)
}
})
.finally(() => setIsOpen(false))
return
}
updateEntry({
endpoint: `/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/{BASE_API_VERSION}/marketplacepanels/${isOpen.metadata.name}`,
body: {
apiVersion: `${BASE_API_GROUP}/${BASE_API_VERSION}`,
kind: 'MarketplacePanel',
metadata: {
name: isOpen.metadata.name,
resourceVersion: isOpen.metadata.resourceVersion,
},
spec: { ...values },
},
})
.then(() => {
setIsOpen(false)
onUpdateSuccess()
})
.catch(err => {
if (isAxiosError(err) || err instanceof Error) {
setError(err)
}
})
.finally(() => setIsOpen(false))
}
const submit = () => {
form
.validateFields()
.then(() => {
onSubmit(form.getFieldsValue())
})
// eslint-disable-next-line no-console
.catch(() => console.log('Validating error'))
}
return (
<Modal
title={typeof isOpen === 'boolean' ? 'Add card' : 'Edit плитку'}
open={isOpen !== false}
onCancel={() => setIsOpen(false)}
onOk={() => submit}
>
<Form<TMarketPlacePanel> form={form} name="control-hooks" initialValues={{ ...defaultValues }}>
<Form.Item label="Name" name="name">
<Input required />
</Form.Item>
<Form.Item label="Description" name="description">
<Input required />
</Form.Item>
<Form.Item label="Icon" name="icon">
<Input.TextArea placeholder="SVG-иконка, <svg /> -> base64" maxLength={undefined} required />
</Form.Item>
<Form.Item label="Resources type" name="type">
<Select
placeholder="Choose resource type"
options={[
{ value: 'direct', label: 'Direct link' },
{ value: 'crd', label: 'CRD' },
{ value: 'nonCrd', label: 'API' },
{ value: 'built-in', label: 'Built-in' },
]}
/>
</Form.Item>
<Form.Item label="Enter API group" name="apiGroup">
<Input disabled={type === 'direct' || type === 'built-in'} />
</Form.Item>
<Form.Item label="Enter API version" name="apiVersion">
<Input disabled={type === 'direct' || type === 'built-in'} />
</Form.Item>
<Form.Item label="Enter resource type" name="typeName">
<Input disabled={type === 'direct'} />
</Form.Item>
<Form.Item label="Enter path" name="pathToNav">
<Input disabled={type !== 'direct'} />
</Form.Item>
<Form.Item label="Tags" name="tags">
<Select
mode="tags"
placeholder="Enter tags. Separators: comma and space"
tokenSeparators={[',', ' ']}
dropdownStyle={{ display: 'none' }}
/>
</Form.Item>
<Form.Item label="Disabled" name="disabled">
<Switch />
</Form.Item>
<Form.Item label="Hidden" name="hidden">
<Switch />
</Form.Item>
</Form>
</Modal>
)
}

View File

@@ -1 +0,0 @@
export * from './AddEditFormModal'

View File

@@ -1,150 +0,0 @@
/* eslint-disable react/no-danger */
import React, { FC } from 'react'
import { useNavigate } from 'react-router-dom'
import { Typography, Flex, theme } from 'antd'
import { useDirectUnknownResource } from '@prorobotech/openapi-k8s-toolkit'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
import { TMarketPlacePanel } from 'localTypes/marketplace'
import { getPathToNav, getListPath } from './utils'
import { Styled } from './styled'
type TMarketplaceCardProps = {
clusterName: string
namespace: string
isEditMode?: boolean
onDeleteClick?: () => void
onEditClick?: () => void
addedMode?: boolean
} & Omit<TMarketPlacePanel, 'hidden'>
export const MarketplaceCard: FC<TMarketplaceCardProps> = ({
description,
name,
icon,
clusterName,
namespace,
type,
pathToNav,
typeName,
apiGroup,
apiVersion,
tags,
disabled,
isEditMode,
onDeleteClick,
onEditClick,
addedMode,
}) => {
const { useToken } = theme
const { token } = useToken()
const navigate = useNavigate()
const baseprefix = useSelector((state: RootState) => state.baseprefix.baseprefix)
let decodedIcon = ''
try {
decodedIcon = window.atob(icon)
} catch {
decodedIcon = "Can't decode"
}
const navigateUrl = getPathToNav({
clusterName,
namespace,
type,
pathToNav,
typeName,
apiGroup,
apiVersion,
baseprefix,
})
const listUrl: string | undefined =
addedMode && type !== 'direct'
? getListPath({
clusterName,
namespace,
type,
typeName,
apiGroup,
apiVersion,
})
: undefined
const { data: k8sList, error: k8sListError } = useDirectUnknownResource<{ items?: [] }>({
uri: listUrl || '',
queryKey: [listUrl || ''],
refetchInterval: false,
isEnabled: addedMode && listUrl !== undefined,
})
if (addedMode && (k8sListError || type === 'direct')) {
return null
}
return (
<Styled.CustomCard
$isDisabled={disabled}
$hoverColor={token.colorPrimary}
onClick={() => (disabled ? null : navigate(navigateUrl))}
>
<Flex vertical style={{ width: '100%', height: '100%' }} justify="spaceBetween">
<Flex justify="space-between">
<Styled.ImageContainer dangerouslySetInnerHTML={{ __html: decodedIcon }} />
{isEditMode && (
<div
onClick={e => {
e.preventDefault()
e.stopPropagation()
if (onDeleteClick) {
onDeleteClick()
}
}}
>
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.5 0C4.70156 0 0 4.70156 0 10.5C0 16.2984 4.70156 21 10.5 21C16.2984 21 21 16.2984 21 10.5C21 4.70156 16.2984 0 10.5 0ZM15 11.0625C15 11.1656 14.9156 11.25 14.8125 11.25H6.1875C6.08437 11.25 6 11.1656 6 11.0625V9.9375C6 9.83438 6.08437 9.75 6.1875 9.75H14.8125C14.9156 9.75 15 9.83438 15 9.9375V11.0625Z"
fill="currentColor"
/>
</svg>
</div>
)}
</Flex>
<Styled.OverflowContainer>
<Styled.TitleContainer>
{name} {addedMode && <span>x{k8sList?.items?.length}</span>}
</Styled.TitleContainer>
<Styled.TagsContainer>
{tags.map(tag => (
<Styled.CustomTag key={tag}>{tag}</Styled.CustomTag>
))}
</Styled.TagsContainer>
<Styled.DescriptionContainer>
<Typography.Text type="secondary">{description}</Typography.Text>
</Styled.DescriptionContainer>
</Styled.OverflowContainer>
<Styled.EditButtonContainer>
{isEditMode && (
<div
onClick={e => {
e.preventDefault()
e.stopPropagation()
if (onEditClick) {
onEditClick()
}
}}
>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.28 16.79H0.72C0.32175 16.79 0 17.1117 0 17.51V18.32C0 18.419 0.081 18.5 0.18 18.5H17.82C17.919 18.5 18 18.419 18 18.32V17.51C18 17.1117 17.6783 16.79 17.28 16.79ZM3.27825 14.9C3.32325 14.9 3.36825 14.8955 3.41325 14.8888L7.19775 14.225C7.24275 14.216 7.2855 14.1958 7.317 14.162L16.8547 4.62425C16.8756 4.60343 16.8922 4.57871 16.9034 4.55149C16.9147 4.52427 16.9205 4.49509 16.9205 4.46562C16.9205 4.43616 16.9147 4.40698 16.9034 4.37976C16.8922 4.35254 16.8756 4.32782 16.8547 4.307L13.1153 0.56525C13.0725 0.5225 13.0163 0.5 12.9555 0.5C12.8948 0.5 12.8385 0.5225 12.7958 0.56525L3.258 10.103C3.22425 10.1367 3.204 10.1772 3.195 10.2222L2.53125 14.0067C2.50936 14.1273 2.51718 14.2513 2.55404 14.3682C2.59089 14.485 2.65566 14.5911 2.74275 14.6772C2.89125 14.8212 3.078 14.9 3.27825 14.9Z"
fill="currentColor"
/>
</svg>
</div>
)}
</Styled.EditButtonContainer>
</Flex>
</Styled.CustomCard>
)
}

View File

@@ -1 +0,0 @@
export * from './MarketplaceCard'

View File

@@ -1,97 +0,0 @@
import styled from 'styled-components'
import { Card, Tag, Typography } from 'antd'
type TCustomCardProps = {
$hoverColor: string
$isDisabled?: boolean
}
const CustomCard = styled(Card)<TCustomCardProps>`
position: relative;
width: 238px;
overflow-x: auto;
cursor: ${({ $isDisabled }) => ($isDisabled ? 'not-allowed' : 'pointer')};
box-shadow:
0 6px 16px 0 #00000014,
0 3px 6px -4px #0000001f,
0 9px 28px 8px #0000000d;
&:hover {
border-color: ${({ $hoverColor, $isDisabled }) => !$isDisabled && $hoverColor};
}
.ant-card-body {
height: 238px;
overflow-x: auto;
padding: 8px;
}
`
const ImageContainer = styled.div`
min-width: 45px;
min-height: 45px;
padding: 6px;
svg {
width: 45px;
height: 45px;
}
`
const OverflowContainer = styled.div`
overflow-x: auto;
scrollbar-width: thin;
margin-bottom: 20px;
`
const TitleContainer = styled(Typography.Text)`
padding-left: 6px;
padding-right: 6px;
font-size: 16px;
line-height: 24px;
span {
font-weight: 700;
}
`
const TagsContainer = styled.div`
margin-top: 6px;
margin-bottom: 6px;
padding-left: 6px;
padding-right: 6px;
display: flex;
flex-flow: row wrap;
align-items: flex-start;
`
const CustomTag = styled(Tag)`
margin-right: 4px;
margin-bottom: 4px;
&:last-child {
margin-right: 0;
}
`
const DescriptionContainer = styled.div`
padding-left: 6px;
padding-right: 6px;
`
const EditButtonContainer = styled.div`
position: absolute;
bottom: 6px;
right: 6px;
`
export const Styled = {
CustomCard,
ImageContainer,
OverflowContainer,
TitleContainer,
TagsContainer,
CustomTag,
DescriptionContainer,
EditButtonContainer,
}

View File

@@ -1,65 +0,0 @@
export const getPathToNav = ({
clusterName,
namespace,
type,
pathToNav,
typeName,
apiGroup,
apiVersion,
baseprefix,
}: {
clusterName: string
namespace: string
type: string
pathToNav?: string
typeName?: string
apiGroup?: string
apiVersion?: string
baseprefix?: string
}): string => {
const apiExtensionVersion = 'v1'
if (type === 'direct' && pathToNav) {
return pathToNav
}
if (type === 'crd') {
return `${baseprefix}/${clusterName}/${namespace}/crd-table/${apiGroup}/${apiVersion}/${apiExtensionVersion}/${typeName}`
}
if (type === 'nonCrd') {
return `${baseprefix}/${clusterName}/${namespace}/api-table/${apiGroup}/${apiVersion}/${typeName}`
}
return `${baseprefix}/${clusterName}/${namespace}/builtin-table/${typeName}`
}
export const getListPath = ({
clusterName,
namespace,
type,
typeName,
apiGroup,
apiVersion,
}: {
clusterName: string
namespace: string
type: string
typeName?: string
apiGroup?: string
apiVersion?: string
}): string | undefined => {
if (type === 'crd') {
return `/api/clusters/${clusterName}/k8s/apis/${apiGroup}/${apiVersion}${
namespace ? `/namespaces/${namespace}` : ''
}/${typeName}`
}
if (type === 'nonCrd') {
return `/api/clusters/${clusterName}/k8s/apis/${apiGroup}/${apiVersion}${
namespace ? `/namespaces/${namespace}` : ''
}/${typeName}`
}
return `/api/clusters/${clusterName}/k8s/api/v1${namespace ? `/namespaces/${namespace}` : ''}/${typeName}`
}

View File

@@ -1,28 +0,0 @@
import React, { FC } from 'react'
import { Select } from 'antd'
type TSearchTextInputProps = {
uniqueTags: string[]
onSelectedTags: (tags: string[]) => void
selectedTags: string[]
}
export const SearchTextInput: FC<TSearchTextInputProps> = ({ uniqueTags, selectedTags, onSelectedTags }) => {
const options = uniqueTags.map(el => ({ key: el, value: el }))
return (
<Select<string[]>
mode="tags"
placeholder="Search"
tokenSeparators={[',', ' ', ' ']}
allowClear
options={options}
onClear={() => {
onSelectedTags([])
}}
value={selectedTags}
onChange={tags => onSelectedTags(tags)}
style={{ width: '240px' }}
/>
)
}

View File

@@ -1 +0,0 @@
export * from './SearchTextInput'

View File

@@ -1,3 +0,0 @@
export * from './AddEditFormModal'
export * from './MarketplaceCard'
export * from './SearchTextInput'

View File

@@ -1,21 +0,0 @@
import styled from 'styled-components'
import { Typography } from 'antd'
const BigValue = styled(Typography.Text)`
font-size: 36px;
line-height: 36px;
font-weight: 700;
/* stylelint-disable-next-line value-no-vendor-prefix */
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-ms-line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-clamp: 1;
word-break: break-all;
`
export const Styled = {
BigValue,
}

View File

@@ -1,3 +1,2 @@
export * from './BlackholeForm'
export * from './ManageableBreadcrumbs'
export * from './MarketPlace'

View File

@@ -17,7 +17,7 @@ import {
} from '@prorobotech/openapi-k8s-toolkit'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
import { BASE_API_GROUP, BASE_API_VERSION, BASE_RPROJECTS_VERSION } from 'constants/customizationApiGroupAndVersion'
import { BASE_API_GROUP, BASE_API_VERSION, BASE_PROJECTS_VERSION } from 'constants/customizationApiGroupAndVersion'
import { FlexGrow } from 'components'
import { TABLE_PROPS } from 'constants/tableProps'
@@ -31,7 +31,7 @@ export const ListProjects: FC = () => {
const path = pathname
const cluster = clusterName || ''
const apiGroup = BASE_API_GROUP
const apiVersion = BASE_RPROJECTS_VERSION
const apiVersion = BASE_PROJECTS_VERSION
const typeName = 'projects'
const isNamespaced = false

View File

@@ -1,21 +0,0 @@
import React, { FC } from 'react'
import { Row, Col } from 'antd'
import { ContentCard, MarketPlace } from 'components'
import { ProjectInfoCard } from './organisms'
export const ProjectInfo: FC = () => {
return (
<Row gutter={[24, 24]} style={{ flexGrow: 1 }}>
<Col span={13}>
<ContentCard flexGrow={1}>
<ProjectInfoCard />
</ContentCard>
</Col>
<Col span={11}>
<ContentCard flexGrow={1}>
<MarketPlace />
</ContentCard>
</Col>
</Row>
)
}

View File

@@ -1 +0,0 @@
export * from './ProjectInfo'

View File

@@ -1,44 +0,0 @@
import React, { FC } from 'react'
import { Button, Dropdown } from 'antd'
export const DropdownAccessGroups: FC = () => {
const getDropdownItems = () => {
return [
'g-advanced-credit-factory',
'g-advanced-credit-factory',
'g-advanced-credit-factory',
'g-advanced-credit-factory',
].map((item, i) => ({
key: `${item}-${i}`,
label: (
<div
onClick={e => {
e.stopPropagation()
}}
>
{item}
</div>
),
}))
}
return (
<Dropdown placement="bottomRight" menu={{ items: getDropdownItems() }} trigger={['click']}>
<Button
type="link"
onClick={e => {
e.preventDefault()
e.stopPropagation()
}}
>
Access Groups
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.8127 3H11.6408C11.5611 3 11.4861 3.03906 11.4393 3.10313L7.0002 9.22188L2.56114 3.10313C2.51426 3.03906 2.43926 3 2.35957 3H1.1877C1.08614 3 1.02676 3.11563 1.08614 3.19844L6.59551 10.7937C6.79551 11.0687 7.20489 11.0687 7.40332 10.7937L12.9127 3.19844C12.9736 3.11563 12.9143 3 12.8127 3Z"
fill="currentColor"
/>
</svg>
</Button>
</Dropdown>
)
}

View File

@@ -1 +0,0 @@
export * from './DropdownAccessGroups'

View File

@@ -1,66 +0,0 @@
import React, { FC } from 'react'
import { Button, Dropdown, Flex } from 'antd'
import { EllipsisOutlined } from '@ant-design/icons'
import { EditIcon, DeleteIcon } from 'components/atoms'
type TDropdownActionsProps = {
onDelete?: () => void
onUpdate?: () => void
}
export const DropdownActions: FC<TDropdownActionsProps> = ({ onDelete, onUpdate }) => {
const getDropdownItems = () => {
const items = []
if (onUpdate) {
items.push({
key: 'update',
label: (
<div
onClick={e => {
e.stopPropagation()
onUpdate()
}}
>
<Flex align="center" gap={8}>
<EditIcon />
Edit
</Flex>
</div>
),
})
}
if (onDelete) {
items.push({
key: 'delete',
label: (
<div
onClick={e => {
e.stopPropagation()
onDelete()
}}
>
<Flex align="center" gap={8}>
<DeleteIcon />
Delete
</Flex>
</div>
),
})
}
return items
}
return (
<Dropdown placement="bottomRight" menu={{ items: getDropdownItems() }} trigger={['click']}>
<Button
type="text"
onClick={e => {
e.preventDefault()
e.stopPropagation()
}}
>
<EllipsisOutlined />
</Button>
</Dropdown>
)
}

View File

@@ -1 +0,0 @@
export * from './DropdownActions'

View File

@@ -1,2 +0,0 @@
export * from './DropdownActions'
export * from './DropdownAccessGroups'

View File

@@ -1,238 +0,0 @@
/* eslint-disable max-lines-per-function */
import React, { FC, useCallback, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Spacer, useDirectUnknownResource, DeleteModal, usePermissions } from '@prorobotech/openapi-k8s-toolkit'
import { Typography, Flex, Spin, Button } from 'antd'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
import { BASE_API_GROUP, BASE_API_VERSION, BASE_RPROJECTS_VERSION } from 'constants/customizationApiGroupAndVersion'
import { TMarketPlacePanelResponse } from 'localTypes/marketplace'
import { MarketplaceCard } from 'components/molecules'
import { DropdownActions, DropdownAccessGroups } from '../../molecules'
import { Styled } from './styled'
export const ProjectInfoCard: FC = () => {
const navigate = useNavigate()
const { clusterName, namespace } = useParams()
const baseprefix = useSelector((state: RootState) => state.baseprefix.baseprefix)
const {
data: marketplacePanels,
isLoading: marketplaceIsLoading,
// error: marketplaceError,
} = useDirectUnknownResource<TMarketPlacePanelResponse>({
uri: `/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/${BASE_API_VERSION}/marketplacepanels/`,
refetchInterval: 5000,
queryKey: ['marketplacePanels', clusterName || 'no-cluster'],
isEnabled: clusterName !== undefined,
})
const {
data: project,
isLoading,
error,
} = useDirectUnknownResource<{
apiVersion: string
kind: 'Project'
metadata: {
labels: {
paas: string
pj: string
}
name: string
resourceVersion: string
uid: string
}
spec: {
businessName?: string
description: string
prefix: string
}
status: {
conditions: {
lastTransitionTime: string
message: string
reason: string
status: string
type: string
}[]
}
}>({
uri: `/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/${BASE_RPROJECTS_VERSION}/projects/${namespace}`,
refetchInterval: 5000,
queryKey: ['projects', clusterName || 'no-cluster'],
isEnabled: clusterName !== undefined,
})
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false)
const updatePermission = usePermissions({
apiGroup: BASE_API_GROUP,
typeName: 'projects',
namespace: '',
clusterName: clusterName || '',
verb: 'update',
refetchInterval: false,
})
const deletePermission = usePermissions({
apiGroup: BASE_API_GROUP,
typeName: 'projects',
namespace: '',
clusterName: clusterName || '',
verb: 'delete',
refetchInterval: false,
})
const openUpdate = useCallback(() => {
navigate(
`${baseprefix}/${clusterName}/forms/apis/${BASE_API_GROUP}/${BASE_RPROJECTS_VERSION}/projects/${namespace}?backlink=${baseprefix}/clusters/${clusterName}`,
)
}, [baseprefix, clusterName, namespace, navigate])
if (isLoading) {
return <Spin />
}
if (!project || error) {
return null
}
const readyCondition = project.status.conditions.find(({ type }) => type === 'Ready')
return (
<>
<Flex justify="space-between">
<div>
<Flex gap={20} vertical>
<div>
<Typography.Text type="secondary">Project Business Name</Typography.Text>
</div>
<div>
<Flex gap="small">
<Styled.BigValue>{project.spec.businessName || '-'}</Styled.BigValue>
{readyCondition && (
<Flex align="center" gap="small">
<Typography.Text type={readyCondition.status === 'True' ? 'success' : 'warning'}>
{readyCondition.reason}
</Typography.Text>
</Flex>
)}
</Flex>
</div>
<div>
<Typography.Text>{project.spec.description}</Typography.Text>
</div>
</Flex>
<Spacer $space={24} $samespace />
<Flex gap={14} vertical>
<div>
<Typography.Text type="secondary">Developer Instruments</Typography.Text>
</div>
<div>
<Flex gap={14} wrap>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
<Button type="link">Test</Button>
</Flex>
</div>
</Flex>
</div>
<div>
<Flex gap={24} vertical>
<Flex justify="flex-end">
{readyCondition?.status === 'True' &&
(updatePermission.data?.status.allowed || deletePermission.data?.status.allowed) ? (
<DropdownActions
onDelete={
deletePermission.data?.status.allowed
? () => {
setIsDeleteModalOpen(true)
}
: undefined
}
onUpdate={updatePermission.data?.status.allowed ? openUpdate : undefined}
/>
) : (
<Styled.ActionMenuPlaceholder />
)}
</Flex>
<DropdownAccessGroups />
</Flex>
</div>
</Flex>
<Spacer $space={24} $samespace />
<Typography.Text type="secondary">Added Products</Typography.Text>
<Spacer $space={12} $samespace />
<Flex gap={22} wrap>
{marketplaceIsLoading && <Spin />}
{clusterName &&
namespace &&
marketplacePanels?.items
.map(({ spec }) => spec)
.sort()
.map(({ name, description, icon, type, pathToNav, typeName, apiGroup, apiVersion, tags, disabled }) => (
<MarketplaceCard
key={name}
description={description}
disabled={disabled}
icon={icon}
isEditMode={false}
name={name}
clusterName={clusterName}
namespace={namespace}
type={type}
pathToNav={pathToNav}
typeName={typeName}
apiGroup={apiGroup}
apiVersion={apiVersion}
tags={tags}
addedMode
/>
))}
</Flex>
{isDeleteModalOpen && (
<DeleteModal
name={project.metadata.name}
onClose={() => {
setIsDeleteModalOpen(false)
navigate(`${baseprefix}/clusters/${clusterName}`)
}}
endpoint={`/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/${BASE_RPROJECTS_VERSION}/projects/${project.metadata.name}`}
/>
)}
</>
)
}

View File

@@ -1 +0,0 @@
export * from './ProjectInfoCard'

View File

@@ -1,27 +0,0 @@
import styled from 'styled-components'
import { Typography } from 'antd'
const ActionMenuPlaceholder = styled.div`
width: 45.33px;
height: 1px;
`
const BigValue = styled(Typography.Text)`
font-size: 36px;
line-height: 36px;
font-weight: 700;
/* stylelint-disable-next-line value-no-vendor-prefix */
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-ms-line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-clamp: 1;
word-break: break-all;
`
export const Styled = {
ActionMenuPlaceholder,
BigValue,
}

View File

@@ -1 +0,0 @@
export * from './ProjectInfoCard'

View File

@@ -1,27 +0,0 @@
import styled from 'styled-components'
import { Typography } from 'antd'
const ActionMenuPlaceholder = styled.div`
width: 45.33px;
height: 1px;
`
const BigValue = styled(Typography.Text)`
font-size: 36px;
line-height: 36px;
font-weight: 700;
/* stylelint-disable-next-line value-no-vendor-prefix */
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-ms-line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
line-clamp: 1;
word-break: break-all;
`
export const Styled = {
ActionMenuPlaceholder,
BigValue,
}

View File

@@ -1,6 +1,5 @@
export * from './ListClusters'
export * from './ListProjects'
export * from './ProjectInfo'
export * from './ListInsideClusterAndNs'
export * from './ListInsideAllResources'
export * from './ListInsideCrdsByApiGroup'

View File

@@ -1,4 +1,7 @@
export const BASE_API_GROUP = import.meta.env.VITE_CUSTOMIZATION_API_GROUP
export const BASE_API_VERSION = import.meta.env.VITE_CUSTOMIZATION_API_VERSION
export const BASE_RPROJECTS_VERSION = import.meta.env.VITE_PROJECTS_VERSION
export const BASE_PROJECTS_VERSION = import.meta.env.VITE_PROJECTS_VERSION
export const BASE_PROJECTS_RESOURCE_NAME = import.meta.env.VITE_PROJECTS_RESOURCE_NAME
export const BASE_MARKETPLACE_RESOURCE_NAME = import.meta.env.VITE_MARKETPLACE_RESOURCE_NAME
export const BASE_MARKETPLACE_KIND = import.meta.env.VITE_MARKETPLACE_KIND
export const BASE_INSTANCES_VERSION = import.meta.env.VITE_INSTANCES_VERSION

View File

@@ -1,5 +1,4 @@
import { TEnrichedTableProps } from '@prorobotech/openapi-k8s-toolkit'
import { EditIcon, DeleteIcon } from 'components/atoms'
import { TEnrichedTableProps, EditIcon, DeleteIcon } from '@prorobotech/openapi-k8s-toolkit'
export const TABLE_PROPS: TEnrichedTableProps['tableProps'] = {
borderless: true,

View File

@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
import {
BASE_API_GROUP,
BASE_RPROJECTS_VERSION,
BASE_PROJECTS_VERSION,
BASE_INSTANCES_VERSION,
} from 'constants/customizationApiGroupAndVersion'
@@ -24,7 +24,7 @@ export const useNavSelector = (clusterName?: string, projectName?: string) => {
clusterName: clusterName || '',
namespace: '',
apiGroup: BASE_API_GROUP,
apiVersion: BASE_RPROJECTS_VERSION,
apiVersion: BASE_PROJECTS_VERSION,
typeName: 'projects',
limit: null,
})

View File

@@ -1,5 +1,6 @@
import React, { FC } from 'react'
import { Factory } from 'components'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ManageableBreadcrumbs, Factory } from 'components'
import { BaseTemplate } from 'templates'
type TFactoryPageProps = {
@@ -9,6 +10,8 @@ type TFactoryPageProps = {
export const FactoryPage: FC<TFactoryPageProps> = ({ forcedTheme }) => {
return (
<BaseTemplate forcedTheme={forcedTheme} withNoCluster>
<ManageableBreadcrumbs />
<Spacer $space={20} $samespace />
<Factory />
</BaseTemplate>
)

View File

@@ -1,10 +1,10 @@
import React, { FC } from 'react'
import { Breadcrumb } from 'antd'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ContentCard, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import type { RootState } from 'store/store'
import { ContentCard, ListClusters } from 'components'
import { ListClusters } from 'components'
import { BaseTemplate } from 'templates'
type TListClustersPageProps = {

View File

@@ -1,10 +1,10 @@
import React, { FC } from 'react'
import { Breadcrumb } from 'antd'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ContentCard, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
import type { RootState } from 'store/store'
import { ContentCard, ListInsideClusterAndNs } from 'components'
import { ListInsideClusterAndNs } from 'components'
import { BaseTemplate } from 'templates'
type TListInsideClustersAndNsPageProps = {

View File

@@ -1,6 +1,6 @@
import React, { FC } from 'react'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ContentCard, ListProjects, ManageableBreadcrumbs } from 'components'
import { ContentCard, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ListProjects, ManageableBreadcrumbs } from 'components'
import { BaseTemplate } from 'templates'
type TListProjectsPageProps = {

View File

@@ -1,6 +1,5 @@
import React, { FC } from 'react'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ProjectInfo, ManageableBreadcrumbs } from 'components'
import { RedirectProjectInfo } from 'components'
import { BaseTemplate } from 'templates'
type TProjectInfoPageProps = {
@@ -10,9 +9,7 @@ type TProjectInfoPageProps = {
export const ProjectInfoPage: FC<TProjectInfoPageProps> = ({ forcedTheme }) => {
return (
<BaseTemplate forcedTheme={forcedTheme}>
<ManageableBreadcrumbs />
<Spacer $space={20} $samespace />
<ProjectInfo />
<RedirectProjectInfo />
</BaseTemplate>
)
}

View File

@@ -1,9 +1,9 @@
import React, { FC } from 'react'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ContentCard, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { useParams, useSearchParams } from 'react-router-dom'
import { useSelector } from 'react-redux'
import type { RootState } from 'store/store'
import { ContentCard, TableNonCrdInfo, BackLink, ManageableBreadcrumbs } from 'components'
import { TableNonCrdInfo, BackLink, ManageableBreadcrumbs } from 'components'
import { BaseTemplate } from 'templates'
import { BASE_API_GROUP, BASE_INSTANCES_VERSION } from 'constants/customizationApiGroupAndVersion'

View File

@@ -1,9 +1,9 @@
import React, { FC } from 'react'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ContentCard, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { useParams, useSearchParams } from 'react-router-dom'
import { useSelector } from 'react-redux'
import type { RootState } from 'store/store'
import { ContentCard, TableBuiltinInfo, BackLink, ManageableBreadcrumbs } from 'components'
import { TableBuiltinInfo, BackLink, ManageableBreadcrumbs } from 'components'
import { BaseTemplate } from 'templates'
import { BASE_API_GROUP, BASE_INSTANCES_VERSION } from 'constants/customizationApiGroupAndVersion'

View File

@@ -1,9 +1,9 @@
import React, { FC } from 'react'
import { Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { ContentCard, Spacer } from '@prorobotech/openapi-k8s-toolkit'
import { useParams } from 'react-router-dom'
import { useSelector } from 'react-redux'
import type { RootState } from 'store/store'
import { ContentCard, TableCrdInfo, BackLink, ManageableBreadcrumbs } from 'components'
import { TableCrdInfo, BackLink, ManageableBreadcrumbs } from 'components'
import { BaseTemplate } from 'templates'
import { BASE_API_GROUP, BASE_INSTANCES_VERSION } from 'constants/customizationApiGroupAndVersion'