mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-27 18:19:50 +00:00
marketplace panels
This commit is contained in:
295
src/components/molecules/MarketPlace/MarketPlace.tsx
Normal file
295
src/components/molecules/MarketPlace/MarketPlace.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
/* 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 } from '@prorobotech/openapi-k8s-toolkit'
|
||||
import { notification, Typography, Card, Flex, Divider, Switch, theme, Spin, Alert } from 'antd'
|
||||
import { BASE_API_GROUP, BASE_API_VERSION } from 'constants/customizationApiGroupAndVersion'
|
||||
import { AddCard } from './atoms'
|
||||
import { AddEditFormModal, CardInProject, SearchTextInput } from './molecules'
|
||||
import {
|
||||
TMarketPlacePanelResponse,
|
||||
TMarketPlacePanelResource,
|
||||
TMarketPlacePanel,
|
||||
TMarketPlaceFiltersAndSorters,
|
||||
} from './types'
|
||||
|
||||
export const MarketPlace: FC = () => {
|
||||
const { useToken } = theme
|
||||
const { token } = useToken()
|
||||
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 [filtersAndSorters, setFiltersAndSorters] = useState<TMarketPlaceFiltersAndSorters>({
|
||||
searchText: '',
|
||||
})
|
||||
|
||||
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(() => {
|
||||
const { searchText, selectedTag } = filtersAndSorters
|
||||
|
||||
let newData = initialData
|
||||
.filter(
|
||||
({ name, description }) =>
|
||||
name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
description.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
.sort()
|
||||
|
||||
if (selectedTag) {
|
||||
newData = newData.filter(({ tags }) => tags.includes(selectedTag))
|
||||
}
|
||||
|
||||
setFilterAndSortedData(newData)
|
||||
}, [filtersAndSorters, initialData])
|
||||
|
||||
if (error) {
|
||||
return <div>{JSON.stringify(error)}</div>
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Spin />
|
||||
}
|
||||
|
||||
if (!marketplacePanels) {
|
||||
return <div>No panels</div>
|
||||
}
|
||||
|
||||
const onSearchTextChange = (searchText: string) => {
|
||||
setFiltersAndSorters({ ...filtersAndSorters, searchText })
|
||||
}
|
||||
|
||||
const onTagSelect = (tag: string) => {
|
||||
if (filtersAndSorters.selectedTag === tag) {
|
||||
setFiltersAndSorters({
|
||||
...filtersAndSorters,
|
||||
selectedTag: undefined,
|
||||
})
|
||||
} else {
|
||||
setFiltersAndSorters({
|
||||
...filtersAndSorters,
|
||||
selectedTag: tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEditMode = () => {
|
||||
setIsEditMode(!isEditMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Flex justify="space-between" align="center" style={{ marginTop: '30px' }}>
|
||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||
Marketplace
|
||||
</Typography.Title>
|
||||
<div>
|
||||
{(createPermission.data?.status.allowed ||
|
||||
updatePermission.data?.status.allowed ||
|
||||
deletePermission.data?.status.allowed) && (
|
||||
<div>
|
||||
<Switch defaultChecked checked={isEditMode} onClick={toggleEditMode} /> Edit Mode
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
{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>
|
||||
<Card style={{ maxWidth: '255px', marginRight: '8px' }}>
|
||||
<Flex justify="center" align="center" vertical>
|
||||
<SearchTextInput searchText={filtersAndSorters.searchText} onSearchTextChange={onSearchTextChange} />
|
||||
<Divider style={{ borderColor: token.colorBorder }} />
|
||||
<Flex justify="center" align="center" vertical style={{ width: '100%' }}>
|
||||
<Typography.Text
|
||||
style={{ width: '100%', marginBottom: '8px', cursor: 'pointer' }}
|
||||
type={!filtersAndSorters.selectedTag ? 'success' : undefined}
|
||||
onClick={() =>
|
||||
setFiltersAndSorters({
|
||||
...filtersAndSorters,
|
||||
selectedTag: undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
All Items
|
||||
</Typography.Text>
|
||||
{uniqueTags.map(tag => (
|
||||
<Typography.Text
|
||||
style={{ width: '100%', marginBottom: '8px', cursor: 'pointer' }}
|
||||
type={filtersAndSorters.selectedTag === tag ? 'success' : undefined}
|
||||
key={tag}
|
||||
onClick={() => onTagSelect(tag)}
|
||||
>
|
||||
{tag}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
<div>
|
||||
<Flex wrap gap="small">
|
||||
{clusterName &&
|
||||
namespace &&
|
||||
filteredAndSortedData.map(
|
||||
({ name, description, icon, type, pathToNav, typeName, apiGroup, apiVersion, tags, disabled }) => (
|
||||
<CardInProject
|
||||
description={description}
|
||||
disabled={disabled}
|
||||
icon={icon}
|
||||
isEditMode={isEditMode}
|
||||
key={name}
|
||||
name={name}
|
||||
clusterName={clusterName}
|
||||
namespace={namespace}
|
||||
type={type}
|
||||
pathToNav={pathToNav}
|
||||
typeName={typeName}
|
||||
apiGroup={apiGroup}
|
||||
apiVersion={apiVersion}
|
||||
// pathToNav={pathToNav}
|
||||
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>
|
||||
</div>
|
||||
</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}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AddCard'
|
||||
14
src/components/molecules/MarketPlace/atoms/AddCard/styled.ts
Normal file
14
src/components/molecules/MarketPlace/atoms/AddCard/styled.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components'
|
||||
import { Card } from 'antd'
|
||||
|
||||
const CustomCard = styled(Card)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: 256px;
|
||||
`
|
||||
|
||||
export const Styled = {
|
||||
CustomCard,
|
||||
}
|
||||
1
src/components/molecules/MarketPlace/atoms/index.ts
Normal file
1
src/components/molecules/MarketPlace/atoms/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './AddCard'
|
||||
1
src/components/molecules/MarketPlace/index.ts
Normal file
1
src/components/molecules/MarketPlace/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './MarketPlace'
|
||||
@@ -0,0 +1,164 @@
|
||||
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 '../../types'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AddEditFormModal'
|
||||
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import React, { FC } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Typography, Flex, theme } from 'antd'
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from 'store/store'
|
||||
import { TitleWithNoTopMargin } from 'components/atoms'
|
||||
import { TMarketPlacePanel } from '../../types'
|
||||
import { getPathToNav } from './utils'
|
||||
import { Styled } from './styled'
|
||||
|
||||
type TCardInProjectProps = {
|
||||
clusterName: string
|
||||
namespace: string
|
||||
isEditMode?: boolean
|
||||
onDeleteClick: () => void
|
||||
onEditClick: () => void
|
||||
} & Omit<TMarketPlacePanel, 'hidden'>
|
||||
|
||||
export const CardInProject: FC<TCardInProjectProps> = ({
|
||||
description,
|
||||
name,
|
||||
icon,
|
||||
clusterName,
|
||||
namespace,
|
||||
type,
|
||||
pathToNav,
|
||||
typeName,
|
||||
apiGroup,
|
||||
apiVersion,
|
||||
tags,
|
||||
disabled,
|
||||
isEditMode,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
}) => {
|
||||
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,
|
||||
})
|
||||
|
||||
return (
|
||||
<Styled.CustomCard
|
||||
$isDisabled={disabled}
|
||||
$hoverColor={token.colorPrimary}
|
||||
onClick={() => (disabled ? null : navigate(navigateUrl))}
|
||||
>
|
||||
<Flex vertical style={{ width: '100%', height: '100%' }} gap="middle" justify="spaceBetween">
|
||||
<Styled.ControlsAndImageContainer>
|
||||
<Styled.ImageContainer dangerouslySetInnerHTML={{ __html: decodedIcon }} />
|
||||
{isEditMode && (
|
||||
<Styled.ControlsContainer>
|
||||
<Styled.ControlsItem>
|
||||
<DeleteOutlined
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDeleteClick()
|
||||
}}
|
||||
/>
|
||||
</Styled.ControlsItem>
|
||||
<Styled.ControlsItem>
|
||||
<EditOutlined
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onEditClick()
|
||||
}}
|
||||
/>
|
||||
</Styled.ControlsItem>
|
||||
</Styled.ControlsContainer>
|
||||
)}
|
||||
</Styled.ControlsAndImageContainer>
|
||||
<TitleWithNoTopMargin level={4}>{name}</TitleWithNoTopMargin>
|
||||
<Styled.FlexGrow>
|
||||
<Styled.TagsContainer>
|
||||
{tags.map(tag => (
|
||||
<Styled.CustomTag key={tag}>{tag}</Styled.CustomTag>
|
||||
))}
|
||||
</Styled.TagsContainer>
|
||||
</Styled.FlexGrow>
|
||||
<Typography.Text type="secondary">{description}</Typography.Text>
|
||||
</Flex>
|
||||
</Styled.CustomCard>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CardInProject'
|
||||
@@ -0,0 +1,80 @@
|
||||
import styled from 'styled-components'
|
||||
import { Card, Tag } from 'antd'
|
||||
|
||||
type TCustomCardProps = {
|
||||
$hoverColor: string
|
||||
$isDisabled?: boolean
|
||||
}
|
||||
|
||||
const CustomCard = styled(Card)<TCustomCardProps>`
|
||||
position: relative;
|
||||
width: 256px;
|
||||
min-height: 256px;
|
||||
cursor: ${({ $isDisabled }) => ($isDisabled ? 'not-allowed' : 'pointer')};
|
||||
|
||||
&:hover {
|
||||
border-color: ${({ $hoverColor, $isDisabled }) => !$isDisabled && $hoverColor};
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
const ControlsAndImageContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
|
||||
svg {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
`
|
||||
|
||||
const ControlsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const ControlsItem = styled.div`
|
||||
margin-right: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const FlexGrow = styled.div`
|
||||
flex-grow: 1;
|
||||
`
|
||||
|
||||
const TagsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-start;
|
||||
`
|
||||
|
||||
const CustomTag = styled(Tag)`
|
||||
padding: 0 12px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`
|
||||
|
||||
export const Styled = {
|
||||
CustomCard,
|
||||
ControlsAndImageContainer,
|
||||
ImageContainer,
|
||||
ControlsContainer,
|
||||
ControlsItem,
|
||||
FlexGrow,
|
||||
TagsContainer,
|
||||
CustomTag,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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}/non-crd-table/${apiGroup}/${apiVersion}/${typeName}`
|
||||
}
|
||||
|
||||
return `${baseprefix}/${clusterName}/${namespace}/builtin-table/${typeName}`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React, { FC } from 'react'
|
||||
import { Input } from 'antd'
|
||||
|
||||
type TSearchTextInputProps = {
|
||||
onSearchTextChange: (searchText: string) => void
|
||||
searchText: string
|
||||
}
|
||||
|
||||
export const SearchTextInput: FC<TSearchTextInputProps> = ({ searchText, onSearchTextChange }) => (
|
||||
<Input
|
||||
placeholder="Поиск"
|
||||
allowClear
|
||||
onClear={() => {
|
||||
onSearchTextChange('')
|
||||
}}
|
||||
value={searchText}
|
||||
onChange={e => onSearchTextChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SearchTextInput'
|
||||
3
src/components/molecules/MarketPlace/molecules/index.ts
Normal file
3
src/components/molecules/MarketPlace/molecules/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './AddEditFormModal'
|
||||
export * from './CardInProject'
|
||||
export * from './SearchTextInput'
|
||||
34
src/components/molecules/MarketPlace/types.ts
Normal file
34
src/components/molecules/MarketPlace/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export type TMarketPlaceFiltersAndSorters = {
|
||||
searchText: string
|
||||
selectedTag?: string
|
||||
}
|
||||
|
||||
export type TMarketPlacePanel = {
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
type: 'crd' | 'nonCrd' | 'built-in' | 'direct'
|
||||
apiGroup?: string
|
||||
apiVersion?: string
|
||||
typeName?: string
|
||||
pathToNav?: string
|
||||
tags: string[]
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
export type TMarketPlacePanelResource = {
|
||||
metadata: {
|
||||
name: string
|
||||
resourceVersion: string
|
||||
uid: string
|
||||
}
|
||||
spec: TMarketPlacePanel
|
||||
}
|
||||
|
||||
export type TMarketPlacePanelResponse = {
|
||||
metadata: {
|
||||
name: string
|
||||
}
|
||||
items: TMarketPlacePanelResource[]
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './BlackholeForm'
|
||||
export * from './ManageableBreadcrumbs'
|
||||
export * from './MarketPlace'
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, Typography, Flex, Row, Col, Spin } from 'antd'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from 'store/store'
|
||||
import { BASE_API_GROUP, BASE_RPROJECTS_VERSION } from 'constants/customizationApiGroupAndVersion'
|
||||
import { MarketPlace } from 'components'
|
||||
import { DropdownActions } from './molecules'
|
||||
import { Styled } from './styled'
|
||||
|
||||
@@ -144,7 +145,7 @@ export const ProjectInfo: FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
Marketplace
|
||||
<MarketPlace />
|
||||
{isDeleteModalOpen && (
|
||||
<DeleteModal
|
||||
name={project.metadata.name}
|
||||
|
||||
Reference in New Issue
Block a user