diff --git a/app/kubernetes/components/yaml-inspector/yamlInspector.html b/app/kubernetes/components/yaml-inspector/yamlInspector.html index 5db527700..bca7d5a4c 100644 --- a/app/kubernetes/components/yaml-inspector/yamlInspector.html +++ b/app/kubernetes/components/yaml-inspector/yamlInspector.html @@ -21,5 +21,7 @@ + + diff --git a/app/kubernetes/components/yaml-inspector/yamlInspectorController.js b/app/kubernetes/components/yaml-inspector/yamlInspectorController.js index e0c737710..dd5986e10 100644 --- a/app/kubernetes/components/yaml-inspector/yamlInspectorController.js +++ b/app/kubernetes/components/yaml-inspector/yamlInspectorController.js @@ -1,4 +1,6 @@ import angular from 'angular'; +import YAML from 'yaml'; +import { FeatureId } from '@/react/portainer/feature-flags/enums'; class KubernetesYamlInspectorController { /* @ngInject */ @@ -8,6 +10,23 @@ class KubernetesYamlInspectorController { this.expanded = false; } + cleanYamlUnwantedFields(yml) { + try { + const ymls = yml.split('---'); + const cleanYmls = ymls.map((yml) => { + const y = YAML.parse(yml); + if (y.metadata) { + delete y.metadata.managedFields; + delete y.metadata.resourceVersion; + } + return YAML.stringify(y); + }); + return cleanYmls.join('---\n'); + } catch (e) { + return yml; + } + } + copyYAML() { this.clipboard.copyText(this.data); $('#copyNotificationYAML').show().fadeOut(2500); @@ -19,6 +38,11 @@ class KubernetesYamlInspectorController { $(selector).css({ height: height }); this.expanded = !this.expanded; } + + $onInit() { + this.data = this.cleanYamlUnwantedFields(this.data); + this.limitedFeature = FeatureId.K8S_EDIT_YAML; + } } export default KubernetesYamlInspectorController; diff --git a/app/kubernetes/react/views/index.ts b/app/kubernetes/react/views/index.ts index 89e9b14c3..d2408b12e 100644 --- a/app/kubernetes/react/views/index.ts +++ b/app/kubernetes/react/views/index.ts @@ -4,6 +4,7 @@ import { r2a } from '@/react-tools/react2angular'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { withReactQuery } from '@/react-tools/withReactQuery'; import { withUIRouter } from '@/react-tools/withUIRouter'; +import { YAMLReplace } from '@/kubernetes/react/views/yamlReplace'; import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressDatatable'; import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView'; @@ -19,4 +20,10 @@ export const viewsModule = angular .component( 'kubernetesIngressesCreateView', r2a(withUIRouter(withReactQuery(withCurrentUser(CreateIngressView))), []) + ) + .component( + 'yamlReplace', + r2a(withUIRouter(withReactQuery(withCurrentUser(YAMLReplace))), [ + 'featureId', + ]) ).name; diff --git a/app/kubernetes/react/views/yamlReplace/index.tsx b/app/kubernetes/react/views/yamlReplace/index.tsx new file mode 100644 index 000000000..1c45da9d2 --- /dev/null +++ b/app/kubernetes/react/views/yamlReplace/index.tsx @@ -0,0 +1,28 @@ +import { FeatureId } from '@/react/portainer/feature-flags/enums'; + +import { Button } from '@@/buttons'; +import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren'; + +interface Props { + featureId: FeatureId; +} +export function YAMLReplace({ featureId }: Props) { + return ( + + + + ); +} diff --git a/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.module.css b/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.module.css new file mode 100644 index 000000000..856845d55 --- /dev/null +++ b/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.module.css @@ -0,0 +1,41 @@ +:global(portainer-tooltip) { + @apply inline-flex; +} + +.tooltip-wrapper { + display: inline-block; + position: relative; +} + +.tooltip { + background-color: var(--bg-tooltip-color) !important; + padding: 0.833em 1em !important; + color: var(--text-tooltip-color) !important; + border-radius: 10px !important; + box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important; + max-width: 400px; + text-align: left; + font-size: 12px !important; + font-weight: 400; +} + +.tooltip:hover { + visibility: visible !important; + opacity: 1 !important; +} + +.tooltip-container { + line-height: 18px; +} + +.tooltip-heading { + font-weight: 500; +} + +.tooltip-beteaser { + color: var(--ui-warning-5); +} + +.tooltip-beteaser:hover { + color: var(--ui-warning-5); +} diff --git a/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx b/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx new file mode 100644 index 000000000..6f0beb3b2 --- /dev/null +++ b/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx @@ -0,0 +1,83 @@ +import ReactTooltip from 'react-tooltip'; +import clsx from 'clsx'; +import _ from 'lodash'; +import ReactDOMServer from 'react-dom/server'; + +import { FeatureId } from '@/react/portainer/feature-flags/enums'; + +import { getFeatureDetails } from '@@/BEFeatureIndicator/utils'; + +import styles from './TooltipWithChildren.module.css'; + +type Position = 'top' | 'right' | 'bottom' | 'left'; + +export interface Props { + position?: Position; + message: string; + className?: string; + children: React.ReactNode; + heading?: string; + BEFeatureID?: FeatureId; +} + +export function TooltipWithChildren({ + message, + position = 'bottom', + className, + children, + heading, + BEFeatureID, +}: Props) { + const id = _.uniqueId('tooltip-'); + + const { url, limitedToBE } = BEFeatureID + ? getFeatureDetails(BEFeatureID) + : { url: '', limitedToBE: false }; + + const messageHTML = ( +
+
+ {heading && {heading}} + {BEFeatureID && limitedToBE && ( + + Business Edition Only + + )} +
+
{message}
+
+ ); + + return ( + + {children} + + + ); +} diff --git a/app/react/components/Tip/TooltipWithChildren/index.ts b/app/react/components/Tip/TooltipWithChildren/index.ts new file mode 100644 index 000000000..42df25cc4 --- /dev/null +++ b/app/react/components/Tip/TooltipWithChildren/index.ts @@ -0,0 +1 @@ +export { TooltipWithChildren } from './TooltipWithChildren'; diff --git a/app/react/components/buttons/Button.css b/app/react/components/buttons/Button.css index aaaf34a79..300982b7f 100644 --- a/app/react/components/buttons/Button.css +++ b/app/react/components/buttons/Button.css @@ -8,3 +8,9 @@ .btn-none:focus { outline: none; } + +.btn-warninglight { + @apply border-warning-5 bg-warning-2 text-black; + @apply th-dark:bg-warning-5 th-dark:bg-opacity-10 th-dark:text-white; + @apply th-highcontrast:bg-warning-5 th-highcontrast:bg-opacity-10 th-highcontrast:text-white; +} diff --git a/app/react/components/buttons/Button.tsx b/app/react/components/buttons/Button.tsx index 19128c7b1..618dae097 100644 --- a/app/react/components/buttons/Button.tsx +++ b/app/react/components/buttons/Button.tsx @@ -21,6 +21,7 @@ type Color = | 'link' | 'light' | 'dangerlight' + | 'warninglight' | 'none'; type Size = 'xsmall' | 'small' | 'medium' | 'large'; diff --git a/app/react/portainer/feature-flags/enums.ts b/app/react/portainer/feature-flags/enums.ts index 7544e8aee..e8887daa3 100644 --- a/app/react/portainer/feature-flags/enums.ts +++ b/app/react/portainer/feature-flags/enums.ts @@ -13,6 +13,7 @@ export enum FeatureId { K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota', K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota', K8S_CREATE_FROM_KUBECONFIG = 'k8s-create-from-kubeconfig', + K8S_EDIT_YAML = 'k8s-edit-yaml', KAAS_PROVISIONING = 'kaas-provisioning', NOMAD = 'nomad', RBAC_ROLES = 'rbac-roles', diff --git a/app/react/portainer/feature-flags/feature-flags.service.ts b/app/react/portainer/feature-flags/feature-flags.service.ts index c108cac25..8f806925c 100644 --- a/app/react/portainer/feature-flags/feature-flags.service.ts +++ b/app/react/portainer/feature-flags/feature-flags.service.ts @@ -37,6 +37,7 @@ export async function init(edition: Edition) { [FeatureId.POD_SECURITY_POLICY_CONSTRAINT]: Edition.BE, [FeatureId.HIDE_DOCKER_HUB_ANONYMOUS]: Edition.BE, [FeatureId.CUSTOM_LOGIN_BANNER]: Edition.BE, + [FeatureId.K8S_EDIT_YAML]: Edition.BE, [FeatureId.ENFORCE_DEPLOYMENT_OPTIONS]: Edition.BE, [FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE, };