Data
diff --git a/src/Shared/Components/CMCS/constants.ts b/src/Shared/Components/CMCS/constants.ts
index d24b0012f..616ccb1e0 100644
--- a/src/Shared/Components/CMCS/constants.ts
+++ b/src/Shared/Components/CMCS/constants.ts
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-import { CMSecretExternalType, CMSecretYamlData } from '@Shared/Services'
+import { CMSecretExternalType } from '@Shared/Services'
+import { KeyValueTableData } from '../KeyValueTable'
import { ConfigMapSecretDataTypeOptionType } from './types'
export const CONFIG_MAP_SECRET_YAML_PARSE_ERROR = 'Please provide valid YAML'
@@ -33,7 +34,7 @@ export const configMapDataTypeOptions: ConfigMapSecretDataTypeOptionType[] = [
{ value: CMSecretExternalType.KubernetesConfigMap, label: 'Kubernetes External ConfigMap' },
]
-export const CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA: CMSecretYamlData[] = [{ k: '', v: '', id: 0 }]
+export const CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA: KeyValueTableData[] = [{ key: '', value: '', id: 0 }]
export const configMapSecretMountDataMap = {
environment: { title: 'Environment Variable', value: 'environment' },
diff --git a/src/Shared/Components/CMCS/utils.ts b/src/Shared/Components/CMCS/utils.ts
index 2d5347d49..6222d34c2 100644
--- a/src/Shared/Components/CMCS/utils.ts
+++ b/src/Shared/Components/CMCS/utils.ts
@@ -26,7 +26,6 @@ import {
CMSecretConfigData,
CMSecretExternalType,
CMSecretPayloadType,
- CMSecretYamlData,
CODE_EDITOR_RADIO_STATE,
ConfigDatum,
ConfigMapSecretUseFormProps,
@@ -36,6 +35,7 @@ import {
} from '@Shared/Services'
import { hasESO, OverrideMergeStrategyType } from '@Pages/index'
+import { KeyValueTableData } from '../KeyValueTable'
import { getSelectPickerOptionByValue } from '../SelectPicker'
import {
CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA,
@@ -93,7 +93,7 @@ export const getSecretDataTypeOptions = (
return isJob ? kubernetesOptions : [...kubernetesOptions, ...esoOptions, ...(isHashiOrAWS ? kesOptions : [])]
}
-const secureValues = (data: Record
, decodeData: boolean): CMSecretYamlData[] => {
+const secureValues = (data: Record, decodeData: boolean): KeyValueTableData[] => {
let decodedData = data || DEFAULT_SECRET_PLACEHOLDER
if (decodeData) {
@@ -104,9 +104,9 @@ const secureValues = (data: Record, decodeData: boolean): CMSecr
}
}
- return Object.keys(decodedData).map((k, id) => ({
- k,
- v: typeof decodedData[k] === 'object' ? YAMLStringify(decodedData[k]) : decodedData[k],
+ return Object.keys(decodedData).map((key, id) => ({
+ key,
+ value: typeof decodedData[key] === 'object' ? YAMLStringify(decodedData[key]) : decodedData[key],
id,
}))
}
@@ -150,8 +150,8 @@ const processExternalSubPathValues = ({
return ''
}
-export const convertKeyValuePairToYAML = (currentData: CMSecretYamlData[]) =>
- currentData.length ? YAMLStringify(currentData.reduce((agg, { k, v }) => ({ ...agg, [k]: v }), {})) : ''
+export const convertKeyValuePairToYAML = (currentData: KeyValueTableData[]) =>
+ currentData.length ? YAMLStringify(currentData.reduce((agg, { key, value }) => ({ ...agg, [key]: value }), {})) : ''
const getSecretDataFromConfigData = ({
secretData,
@@ -368,29 +368,30 @@ export const getConfigMapSecretReadOnlyValues = ({
? [
{
displayName: 'Keys',
- value: currentData?.length > 0 ? currentData.map((d) => d.k).join(', ') : 'No keys available',
+ value:
+ currentData?.length > 0 ? currentData.map((d) => d.key).join(', ') : 'No keys available',
key: 'keys',
},
]
: []),
],
- data: !mountExistingExternal ? (currentData?.[0]?.k && yaml) || esoSecretYaml || secretDataYaml : null,
+ data: !mountExistingExternal ? (currentData?.[0]?.key && yaml) || esoSecretYaml || secretDataYaml : null,
}
}
-export const convertYAMLToKeyValuePair = (yaml: string): CMSecretYamlData[] => {
+export const convertYAMLToKeyValuePair = (yaml: string): KeyValueTableData[] => {
try {
const obj = yaml && YAML.parse(yaml)
if (typeof obj !== 'object') {
throw new Error()
}
- const keyValueArray: CMSecretYamlData[] = Object.keys(obj).reduce((agg, k, id) => {
- if (!k && !obj[k]) {
+ const keyValueArray = Object.keys(obj).reduce((agg, key, id) => {
+ if (!key && !obj[key]) {
return CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA
}
- const v = obj[k] && typeof obj[k] === 'object' ? YAMLStringify(obj[k]) : obj[k].toString()
+ const value = obj[key] && typeof obj[key] === 'object' ? YAMLStringify(obj[key]) : obj[key].toString()
- return [...agg, { k, v: v ?? '', id }]
+ return [...agg, { key, value: value ?? '', id }]
}, [])
return keyValueArray
} catch {
@@ -449,14 +450,14 @@ export const getConfigMapSecretPayload = ({
const isESO = isSecret && hasESO(externalType)
const _currentData = yamlMode ? convertYAMLToKeyValuePair(yaml) : currentData
const data = _currentData.reduce((acc, curr) => {
- if (!curr.k) {
+ if (!curr.key) {
return acc
}
- const value = curr.v ?? ''
+ const value = curr.value ?? ''
return {
...acc,
- [curr.k]: isSecret && externalType === '' ? btoa(value) : value,
+ [curr.key]: isSecret && externalType === '' ? btoa(value) : value,
}
}, {})
diff --git a/src/Shared/Components/CMCS/validations.ts b/src/Shared/Components/CMCS/validations.ts
index f1858649a..fa76dfe12 100644
--- a/src/Shared/Components/CMCS/validations.ts
+++ b/src/Shared/Components/CMCS/validations.ts
@@ -19,10 +19,11 @@ import YAML from 'yaml'
import { PATTERNS } from '@Common/Constants'
import { YAMLStringify } from '@Common/Helper'
import { UseFormValidation, UseFormValidations } from '@Shared/Hooks'
-import { CMSecretExternalType, CMSecretYamlData, ConfigMapSecretUseFormProps } from '@Shared/Services'
+import { CMSecretExternalType, ConfigMapSecretUseFormProps } from '@Shared/Services'
import { validateCMVolumeMountPath } from '@Shared/validations'
import { hasESO } from '@Pages/index'
+import { KeyValueTableData } from '../KeyValueTable'
import { CONFIG_MAP_SECRET_YAML_PARSE_ERROR, SECRET_TOAST_INFO } from './constants'
import { getESOSecretDataFromYAML } from './utils'
@@ -310,7 +311,7 @@ export const getConfigMapSecretFormValidations: UseFormValidations !!value.filter(({ k }) => !!k).length,
+ isValid: (value: KeyValueTableData[]) => !!value.filter(({ key }) => !!key).length,
message: 'This is a required field',
},
},
diff --git a/src/Common/CodeMirror/CodeEditor.components.tsx b/src/Shared/Components/CodeEditor/CodeEditor.components.tsx
similarity index 100%
rename from src/Common/CodeMirror/CodeEditor.components.tsx
rename to src/Shared/Components/CodeEditor/CodeEditor.components.tsx
diff --git a/src/Common/CodeMirror/CodeEditor.constants.ts b/src/Shared/Components/CodeEditor/CodeEditor.constants.ts
similarity index 100%
rename from src/Common/CodeMirror/CodeEditor.constants.ts
rename to src/Shared/Components/CodeEditor/CodeEditor.constants.ts
diff --git a/src/Common/CodeMirror/CodeEditor.context.ts b/src/Shared/Components/CodeEditor/CodeEditor.context.ts
similarity index 100%
rename from src/Common/CodeMirror/CodeEditor.context.ts
rename to src/Shared/Components/CodeEditor/CodeEditor.context.ts
diff --git a/src/Common/CodeMirror/CodeEditor.theme.ts b/src/Shared/Components/CodeEditor/CodeEditor.theme.ts
similarity index 100%
rename from src/Common/CodeMirror/CodeEditor.theme.ts
rename to src/Shared/Components/CodeEditor/CodeEditor.theme.ts
diff --git a/src/Common/CodeMirror/CodeEditor.tsx b/src/Shared/Components/CodeEditor/CodeEditor.tsx
similarity index 100%
rename from src/Common/CodeMirror/CodeEditor.tsx
rename to src/Shared/Components/CodeEditor/CodeEditor.tsx
diff --git a/src/Common/CodeMirror/CodeEditorRenderer.tsx b/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx
similarity index 100%
rename from src/Common/CodeMirror/CodeEditorRenderer.tsx
rename to src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx
diff --git a/src/Common/CodeMirror/Commands/findAndReplace.ts b/src/Shared/Components/CodeEditor/Commands/findAndReplace.ts
similarity index 100%
rename from src/Common/CodeMirror/Commands/findAndReplace.ts
rename to src/Shared/Components/CodeEditor/Commands/findAndReplace.ts
diff --git a/src/Common/CodeMirror/Commands/index.ts b/src/Shared/Components/CodeEditor/Commands/index.ts
similarity index 100%
rename from src/Common/CodeMirror/Commands/index.ts
rename to src/Shared/Components/CodeEditor/Commands/index.ts
diff --git a/src/Common/CodeMirror/Commands/keyMaps.ts b/src/Shared/Components/CodeEditor/Commands/keyMaps.ts
similarity index 100%
rename from src/Common/CodeMirror/Commands/keyMaps.ts
rename to src/Shared/Components/CodeEditor/Commands/keyMaps.ts
diff --git a/src/Common/CodeMirror/Extensions/DiffMinimap.tsx b/src/Shared/Components/CodeEditor/Extensions/DiffMinimap.tsx
similarity index 100%
rename from src/Common/CodeMirror/Extensions/DiffMinimap.tsx
rename to src/Shared/Components/CodeEditor/Extensions/DiffMinimap.tsx
diff --git a/src/Common/CodeMirror/Extensions/findAndReplace.tsx b/src/Shared/Components/CodeEditor/Extensions/findAndReplace.tsx
similarity index 100%
rename from src/Common/CodeMirror/Extensions/findAndReplace.tsx
rename to src/Shared/Components/CodeEditor/Extensions/findAndReplace.tsx
diff --git a/src/Common/CodeMirror/Extensions/index.ts b/src/Shared/Components/CodeEditor/Extensions/index.ts
similarity index 100%
rename from src/Common/CodeMirror/Extensions/index.ts
rename to src/Shared/Components/CodeEditor/Extensions/index.ts
diff --git a/src/Common/CodeMirror/Extensions/readOnlyTooltip.ts b/src/Shared/Components/CodeEditor/Extensions/readOnlyTooltip.ts
similarity index 100%
rename from src/Common/CodeMirror/Extensions/readOnlyTooltip.ts
rename to src/Shared/Components/CodeEditor/Extensions/readOnlyTooltip.ts
diff --git a/src/Common/CodeMirror/Extensions/yamlHighlight.ts b/src/Shared/Components/CodeEditor/Extensions/yamlHighlight.ts
similarity index 100%
rename from src/Common/CodeMirror/Extensions/yamlHighlight.ts
rename to src/Shared/Components/CodeEditor/Extensions/yamlHighlight.ts
diff --git a/src/Common/CodeMirror/Extensions/yamlParseLinter.ts b/src/Shared/Components/CodeEditor/Extensions/yamlParseLinter.ts
similarity index 100%
rename from src/Common/CodeMirror/Extensions/yamlParseLinter.ts
rename to src/Shared/Components/CodeEditor/Extensions/yamlParseLinter.ts
diff --git a/src/Common/CodeMirror/codeEditor.scss b/src/Shared/Components/CodeEditor/codeEditor.scss
similarity index 99%
rename from src/Common/CodeMirror/codeEditor.scss
rename to src/Shared/Components/CodeEditor/codeEditor.scss
index 202fa4ee0..b25e43aef 100644
--- a/src/Common/CodeMirror/codeEditor.scss
+++ b/src/Shared/Components/CodeEditor/codeEditor.scss
@@ -175,6 +175,10 @@
max-width: 300px;
}
+ .cm-tooltip-autocomplete {
+ background-color: var(--bg-primary);
+ }
+
.cm-diagnostic-error {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
diff --git a/src/Common/CodeMirror/index.ts b/src/Shared/Components/CodeEditor/index.ts
similarity index 100%
rename from src/Common/CodeMirror/index.ts
rename to src/Shared/Components/CodeEditor/index.ts
diff --git a/src/Common/CodeMirror/types.ts b/src/Shared/Components/CodeEditor/types.ts
similarity index 100%
rename from src/Common/CodeMirror/types.ts
rename to src/Shared/Components/CodeEditor/types.ts
diff --git a/src/Common/CodeMirror/utils.tsx b/src/Shared/Components/CodeEditor/utils.tsx
similarity index 100%
rename from src/Common/CodeMirror/utils.tsx
rename to src/Shared/Components/CodeEditor/utils.tsx
diff --git a/src/Shared/Components/CodeEditorWrapper/CodeEditorWrapper.tsx b/src/Shared/Components/CodeEditorWrapper/CodeEditorWrapper.tsx
deleted file mode 100644
index c28e1bd06..000000000
--- a/src/Shared/Components/CodeEditorWrapper/CodeEditorWrapper.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { CodeEditor } from '@Common/CodeEditor'
-import { CodeEditor as CodeMirror, CodeEditorHeaderProps, CodeEditorStatusBarProps } from '@Common/CodeMirror'
-
-import { CodeEditorWrapperProps } from './types'
-
-export const isCodeMirrorEnabled = () => window._env_.FEATURE_CODE_MIRROR_ENABLE
-
-export const CodeEditorWrapper = ({
- codeEditorProps,
- codeMirrorProps,
- children,
- ...restProps
-}: CodeEditorWrapperProps) =>
- isCodeMirrorEnabled() ? (
- {...(codeMirrorProps as any)} {...restProps}>
- {children}
-
- ) : (
-
- {children}
-
- )
-
-const CodeEditorLanguageChangerWrapper = () => (isCodeMirrorEnabled() ? null : )
-
-const CodeEditorThemeChangerWrapper = () => (isCodeMirrorEnabled() ? null : )
-
-const CodeEditorValidationErrorWrapper = () => (isCodeMirrorEnabled() ? null : )
-
-const CodeEditorClipboardWrapper = () => (isCodeMirrorEnabled() ? : )
-
-const CodeEditorHeaderWrapper = (props: CodeEditorHeaderProps) =>
- isCodeMirrorEnabled() ? :
-
-const CodeEditorWarningWrapper = (props: CodeEditorStatusBarProps) =>
- isCodeMirrorEnabled() ? :
-
-const CodeEditorErrorBarWrapper = (props: CodeEditorStatusBarProps) =>
- isCodeMirrorEnabled() ? :
-
-const CodeEditorInformationWrapper = (props: CodeEditorStatusBarProps) =>
- isCodeMirrorEnabled() ? :
-
-const CodeEditorContainerWrapper = ({
- overflowHidden,
- ...props
-}: {
- children: React.ReactNode
- flexExpand?: boolean
- overflowHidden?: boolean
-}) =>
- isCodeMirrorEnabled() ? (
-
- ) : (
-
- )
-
-CodeEditorWrapper.LanguageChanger = CodeEditorLanguageChangerWrapper
-CodeEditorWrapper.ThemeChanger = CodeEditorThemeChangerWrapper
-CodeEditorWrapper.ValidationError = CodeEditorValidationErrorWrapper
-CodeEditorWrapper.Clipboard = CodeEditorClipboardWrapper
-CodeEditorWrapper.Header = CodeEditorHeaderWrapper
-CodeEditorWrapper.Warning = CodeEditorWarningWrapper
-CodeEditorWrapper.ErrorBar = CodeEditorErrorBarWrapper
-CodeEditorWrapper.Information = CodeEditorInformationWrapper
-CodeEditorWrapper.Container = CodeEditorContainerWrapper
diff --git a/src/Shared/Components/CodeEditorWrapper/index.ts b/src/Shared/Components/CodeEditorWrapper/index.ts
deleted file mode 100644
index 0a036a17c..000000000
--- a/src/Shared/Components/CodeEditorWrapper/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export { CodeEditorWrapper as CodeEditor, isCodeMirrorEnabled } from './CodeEditorWrapper'
diff --git a/src/Shared/Components/CodeEditorWrapper/types.ts b/src/Shared/Components/CodeEditorWrapper/types.ts
deleted file mode 100644
index b69233712..000000000
--- a/src/Shared/Components/CodeEditorWrapper/types.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { CodeEditorInterface } from '@Common/CodeEditor'
-import { CodeEditorProps } from '@Common/CodeMirror'
-
-export type CodeEditorWrapperProps = Pick<
- CodeEditorProps,
- | 'mode'
- | 'tabSize'
- | 'readOnly'
- | 'placeholder'
- | 'noParsing'
- | 'loading'
- | 'customLoader'
- | 'cleanData'
- | 'disableSearch'
- | 'children'
-> & {
- diffView?: DiffView
- codeEditorProps: Omit<
- CodeEditorInterface,
- | 'mode'
- | 'tabSize'
- | 'readOnly'
- | 'placeholder'
- | 'noParsing'
- | 'loading'
- | 'customLoader'
- | 'cleanData'
- | 'disableSearch'
- | 'children'
- >
- codeMirrorProps: Omit<
- CodeEditorProps,
- | 'mode'
- | 'tabSize'
- | 'readOnly'
- | 'placeholder'
- | 'noParsing'
- | 'loading'
- | 'customLoader'
- | 'cleanData'
- | 'disableSearch'
- | 'children'
- >
-}
diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
index b98cf41bd..95349fc75 100644
--- a/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
+++ b/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { useMemo } from 'react'
+import { useMemo, useState } from 'react'
import { DynamicDataTableHeader } from './DynamicDataTableHeader'
import { DynamicDataTableRow } from './DynamicDataTableRow'
@@ -24,14 +24,30 @@ import './styles.scss'
export const DynamicDataTable = >({
headers,
+ onRowAdd,
...props
}: DynamicDataTableProps) => {
+ // STATES
+ const [isAddRowButtonClicked, setIsAddRowButtonClicked] = useState(false)
+
+ // CONSTANTS
const filteredHeaders = useMemo(() => headers.filter(({ isHidden }) => !isHidden), [headers])
+ // HANDLERS
+ const handleRowAdd = () => {
+ setIsAddRowButtonClicked(true)
+ onRowAdd()
+ }
+
return (
-
-
+
+
)
}
diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx
index a6d59cb91..e0f6eb83a 100644
--- a/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx
+++ b/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx
@@ -27,6 +27,7 @@ export const DynamicDataTableHeader = }
variant={ButtonVariantType.borderLess}
size={ComponentSizeType.xs}
- showAriaLabelInTippy={false}
/>
)}
{key === lastHeaderKey && headerComponent}
diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
index 3904c5147..45b141d80 100644
--- a/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
+++ b/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
@@ -73,6 +73,8 @@ export const DynamicDataTableRow = ) => {
// CONSTANTS
const isFirstRowEmpty = headers.every(({ key }) => !rows[0]?.data[key].value)
@@ -105,6 +107,8 @@ export const DynamicDataTableRow = {
setIsRowAdded(rows.length > 0 && Object.keys(cellRef.current).length < rows.length)
+ // When a new row is added, we create references for its cells.
+ // This logic ensures that references are created only for the newly added row, while retaining the existing references.
const updatedCellRef = rowIds.reduce((acc, curr) => {
if (cellRef.current[curr]) {
acc[curr] = cellRef.current[curr]
@@ -118,15 +122,16 @@ export const DynamicDataTableRow = {
- if (isRowAdded) {
+ if (isAddRowButtonClicked && isRowAdded) {
// Using the below logic to ensure the cell is focused after row addition.
const cell = cellRef.current[rows[0].id][focusableFieldKey || headers[0].key].current
if (cell) {
cell.focus()
setIsRowAdded(false)
+ setIsAddRowButtonClicked(false)
}
}
- }, [isRowAdded])
+ }, [isRowAdded, isAddRowButtonClicked])
// METHODS
const onChange =
@@ -290,16 +295,18 @@ export const DynamicDataTableRow =
{errorMessages.map((error) => renderErrorMessage(error))}
diff --git a/src/Shared/Components/DynamicDataTable/types.ts b/src/Shared/Components/DynamicDataTable/types.ts
index 8d3785d17..3e9574cb1 100644
--- a/src/Shared/Components/DynamicDataTable/types.ts
+++ b/src/Shared/Components/DynamicDataTable/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { DetailedHTMLProps, ReactElement, ReactNode } from 'react'
+import { DetailedHTMLProps, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react'
import { ResizableTagTextAreaProps } from '@Common/CustomTagSelector'
import { UseStateFiltersReturnType } from '@Common/Hooks'
@@ -169,6 +169,10 @@ export type DynamicDataTableProps