diff --git a/catalog/helm/templates/oauth-proxy/deployment.yaml b/catalog/helm/templates/oauth-proxy/deployment.yaml index dc9c7871a..c1cfcbd9b 100644 --- a/catalog/helm/templates/oauth-proxy/deployment.yaml +++ b/catalog/helm/templates/oauth-proxy/deployment.yaml @@ -42,8 +42,10 @@ spec: - --logout-url=/ - --provider=openshift - --skip-auth-regex=^/(api/)?workshop($|/.*) + - --skip-auth-regex=^/support($|/) - --skip-auth-regex=^/fonts/.* - --skip-auth-regex=^/images/.* + - --skip-auth-regex=^/public/.* - --skip-auth-regex=(.js|.html|.css|.map|.txt)($|\?.*) - --skip-provider-button=true - --tls-cert=/etc/tls/private/tls.crt diff --git a/catalog/ui/src/app/Catalog/CatalogItemCard.tsx b/catalog/ui/src/app/Catalog/CatalogItemCard.tsx index 4e3a3f092..828a4d5a1 100644 --- a/catalog/ui/src/app/Catalog/CatalogItemCard.tsx +++ b/catalog/ui/src/app/Catalog/CatalogItemCard.tsx @@ -1,11 +1,20 @@ import React from 'react'; import { Link, useLocation, useParams } from 'react-router-dom'; -import { Badge, CardBody, CardHeader, Split, SplitItem, Title } from '@patternfly/react-core'; +import { Badge, CardBody, CardHeader, Split, SplitItem, Title, Tooltip } from '@patternfly/react-core'; import { CatalogItem } from '@app/types'; -import CatalogItemIcon from './CatalogItemIcon'; -import { formatString, getDescription, getIsDisabled, getProvider, getStage, getStatus } from './catalog-utils'; import StatusPageIcons from '@app/components/StatusPageIcons'; import { displayName, renderContent, stripHtml } from '@app/util'; +import EnterprisePremiumIcon from '@app/Support/EnterprisePremiumIcon'; +import { + formatString, + getDescription, + getIsDisabled, + getProvider, + getStage, + getStatus, + getSupportType, +} from './catalog-utils'; +import CatalogItemIcon from './CatalogItemIcon'; import './catalog-item-card.css'; @@ -18,6 +27,7 @@ const CatalogItemCard: React.FC<{ catalogItem: CatalogItem }> = ({ catalogItem } const stage = getStage(catalogItem); const isDisabled = getIsDisabled(catalogItem); const { code: status } = getStatus(catalogItem); + const supportType = getSupportType(catalogItem); if (!urlSearchParams.has('item')) { if (namespace) { @@ -28,41 +38,52 @@ const CatalogItemCard: React.FC<{ catalogItem: CatalogItem }> = ({ catalogItem } } return ( - - - - - - {status && status !== 'operational' ? ( - - ) : null} - - - {stage === 'dev' ? ( - development - ) : stage === 'test' ? ( - test +
+ {supportType && stage === 'prod' ? ( + {supportType.replace(/_+/g, ' ')}

}> + + {supportType === 'Enterprise_Premium' ? ( + ) : null} - - - - - - {displayName(catalogItem)} - - - provided by {formatString(provider)} - -
- {description - ? stripHtml(renderContent(description, { format: descriptionFormat })).slice(0, 150) - : 'No description available.'} -
-
- +
+
+ ) : null} + + + + + + {status && status !== 'operational' ? ( + + ) : null} + + + {stage === 'dev' ? ( + development + ) : stage === 'test' ? ( + test + ) : null} + + + + + + {displayName(catalogItem)} + + + provided by {formatString(provider)} + +
+ {description + ? stripHtml(renderContent(description, { format: descriptionFormat })).slice(0, 150) + : 'No description available.'} +
+
+ +
); }; diff --git a/catalog/ui/src/app/Catalog/CatalogItemForm.spec.tsx b/catalog/ui/src/app/Catalog/CatalogItemForm.spec.tsx index 50bec3c65..6540be365 100644 --- a/catalog/ui/src/app/Catalog/CatalogItemForm.spec.tsx +++ b/catalog/ui/src/app/Catalog/CatalogItemForm.spec.tsx @@ -37,7 +37,7 @@ describe('CatalogItemForm Component', () => { expect(catalogItemDisplayName).toBeInTheDocument(); expect(sfidLabel).toBeInTheDocument(); expect(purposeLabel.closest('.pf-c-form__group').textContent).toContain(purposePlaceholder); - expect(termsOfServiceLabel.closest('.catalog-terms-of-service').textContent).toContain(termsOfServiceAck); + expect(termsOfServiceLabel.closest('.terms-of-service').textContent).toContain(termsOfServiceAck); }); test('When Cancel button is clicked the history goBack function is called', async () => { diff --git a/catalog/ui/src/app/Catalog/catalog-item-card.css b/catalog/ui/src/app/Catalog/catalog-item-card.css index d70a2cf7f..0848c2d4d 100644 --- a/catalog/ui/src/app/Catalog/catalog-item-card.css +++ b/catalog/ui/src/app/Catalog/catalog-item-card.css @@ -1,3 +1,7 @@ +.catalog-item-card__wrapper { + position: relative; +} + .catalog-item-card { color: var(--pf-global--Color--100); display: flex; @@ -9,6 +13,44 @@ padding: var(--pf-global--spacer--lg); overflow: hidden; } +.catalog-item-card__support-type:before { + transform: rotate(-45deg); + left: -3px; + content: ''; + width: 0px; + height: 0px; + position: absolute; + border: 4px solid transparent; + border-top-color: var(--pf-global--danger-color--200); + top: 2px; +} +.catalog-item-card__support-type { + top: -6px; + right: var(--pf-global--spacer--lg); + padding: 8px; + width: 32px; + margin: auto; + box-sizing: border-box; + background-color: var(--pf-global--danger-color--100); + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + color: #ffffff; + position: absolute; + z-index: 1; + box-shadow: 0px 5px 10px rgb(0 0 0 / 25%); + height: 42px; +} + +.catalog-iten-card__support-type-icon { + width: 32px; + position: absolute; + padding: 10px; + left: 0; + top: 0; +} + .catalog-item-card--disabled { opacity: 0.45; } diff --git a/catalog/ui/src/app/Catalog/catalog-item-form.css b/catalog/ui/src/app/Catalog/catalog-item-form.css index df062c3ed..b5f75c09a 100644 --- a/catalog/ui/src/app/Catalog/catalog-item-form.css +++ b/catalog/ui/src/app/Catalog/catalog-item-form.css @@ -12,17 +12,17 @@ align-items: center; } -.catalog-item-form__form .catalog-terms-of-service { +.catalog-item-form__form .terms-of-service { background-color: var(--pf-global--BackgroundColor--light-300); padding: var(--pf-global--spacer--md); } -.catalog-item-form__form .catalog-terms-of-service .pf-c-form__label { +.catalog-item-form__form .terms-of-service .pf-c-form__label { cursor: default; } .catalog-item-form__group-control--single, .catalog-item-form__group-control--multi, -.catalog-item-form__form .catalog-terms-of-service .pf-c-form__group-control { +.catalog-item-form__form .terms-of-service .pf-c-form__group-control { display: flex; flex-direction: row; gap: var(--pf-global--spacer--md); @@ -31,7 +31,7 @@ .catalog-item-form__group-control--multi { padding-bottom: var(--pf-global--spacer--sm); } -.catalog-item-form__form .catalog-terms-of-service .pf-c-form__group-control { +.catalog-item-form__form .terms-of-service .pf-c-form__group-control { flex-direction: column; align-items: flex-start; } diff --git a/catalog/ui/src/app/Catalog/catalog-item-request.css b/catalog/ui/src/app/Catalog/catalog-item-request.css index 56195dbf8..264ad6088 100644 --- a/catalog/ui/src/app/Catalog/catalog-item-request.css +++ b/catalog/ui/src/app/Catalog/catalog-item-request.css @@ -8,17 +8,17 @@ align-items: center; } -.catalog-item-request .catalog-terms-of-service { +.catalog-item-request .terms-of-service { background-color: var(--pf-global--BackgroundColor--light-300); padding: var(--pf-global--spacer--md); } -.catalog-terms-of-service .pf-c-form__label { +.terms-of-service .pf-c-form__label { cursor: default; } .catalog-item-request__group-control--single, .catalog-item-request__group-control--multi, -.catalog-terms-of-service .pf-c-form__group-control { +.terms-of-service .pf-c-form__group-control { display: flex; flex-direction: row; gap: var(--pf-global--spacer--md); @@ -27,7 +27,7 @@ .catalog-item-request__group-control--multi { padding-bottom: var(--pf-global--spacer--sm); } -.catalog-terms-of-service .pf-c-form__group-control { +.terms-of-service .pf-c-form__group-control { flex-direction: column; align-items: flex-start; } diff --git a/catalog/ui/src/app/Catalog/catalog-utils.ts b/catalog/ui/src/app/Catalog/catalog-utils.ts index 430112e9d..e61ca9dda 100644 --- a/catalog/ui/src/app/Catalog/catalog-utils.ts +++ b/catalog/ui/src/app/Catalog/catalog-utils.ts @@ -22,10 +22,19 @@ export function getDescription(catalogItem: CatalogItem): { (catalogItem.metadata.annotations?.[`${BABYLON_DOMAIN}/descriptionFormat`] as 'html' | 'asciidoc') || 'asciidoc', }; } + export function getStage(catalogItem: CatalogItem): string | null { return catalogItem.metadata.labels?.[`${BABYLON_DOMAIN}/stage`]; } +const supportedSupportTypes = ['Enterprise_Premium', 'Enterprise_Standard', 'Community'] as const; +type SupportTypes = typeof supportedSupportTypes[number]; +export function getSupportType(catalogItem: CatalogItem): SupportTypes { + const supportType = catalogItem.metadata.labels?.[`${BABYLON_DOMAIN}/Support_Type`] as SupportTypes; + if (!supportedSupportTypes.includes(supportType)) return null; + return supportType; +} + export function getIsDisabled(catalogItem: CatalogItem): boolean { if (catalogItem.metadata.labels?.[`${BABYLON_DOMAIN}/disabled`]) { return catalogItem.metadata.labels?.[`${BABYLON_DOMAIN}/disabled`] === 'true'; @@ -97,5 +106,5 @@ export function setLastFilter(filter: string): void { export function formatString(string: string): string { return (string.charAt(0).toUpperCase() + string.slice(1)).replace(/_/g, ' '); } -export const HIDDEN_LABELS = ['disabled', 'userCatalogItem', 'stage']; +export const HIDDEN_LABELS = ['disabled', 'userCatalogItem', 'stage', 'Support_Type']; export const HIDDEN_ANNOTATIONS = ['ops', 'displayNameComponent0', 'displayNameComponent1']; diff --git a/catalog/ui/src/app/Header/PublicHeader.tsx b/catalog/ui/src/app/Header/PublicHeader.tsx new file mode 100644 index 000000000..10f0b3191 --- /dev/null +++ b/catalog/ui/src/app/Header/PublicHeader.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ApplicationLauncher, + ApplicationLauncherItem, + DropdownPosition, + PageHeader, + PageHeaderTools, +} from '@patternfly/react-core'; +import { QuestionCircleIcon } from '@patternfly/react-icons'; +import UserInterfaceLogo from '@app/components/UserInterfaceLogo'; + +import './header.css'; + +const PublicHeader: React.FC = () => { + const [isUserHelpDropdownOpen, setUserHelpDropdownOpen] = useState(false); + const navigate = useNavigate(); + + function LogoImg() { + return navigate('/')} style={{ width: '278px' }} />; + } + const openSupportCase = (e: { preventDefault: () => void }) => { + e.preventDefault(); + window.open('https://red.ht/open-support', '_blank'); + }; + + const UserHelpDropdownItems = [ + + Open Support Case + , + + Status Page + , + ]; + + const HeaderTools = ( + + setUserHelpDropdownOpen((prevIsOpen) => !prevIsOpen)} + onToggle={(isOpen: boolean) => setUserHelpDropdownOpen(isOpen)} + isOpen={isUserHelpDropdownOpen} + items={UserHelpDropdownItems} + position={DropdownPosition.right} + toggleIcon={} + /> + + ); + + return } className="public-header-component" headerTools={HeaderTools} />; +}; + +export default PublicHeader; diff --git a/catalog/ui/src/app/Support/EnterprisePremiumIcon.tsx b/catalog/ui/src/app/Support/EnterprisePremiumIcon.tsx new file mode 100644 index 000000000..9b3879fc3 --- /dev/null +++ b/catalog/ui/src/app/Support/EnterprisePremiumIcon.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const EnterprisePremiumIcon: React.FC & { fill?: string }> = (props) => ( + + + +); + +export default EnterprisePremiumIcon; diff --git a/catalog/ui/src/app/Support/SupportPage.tsx b/catalog/ui/src/app/Support/SupportPage.tsx new file mode 100644 index 000000000..540da2d59 --- /dev/null +++ b/catalog/ui/src/app/Support/SupportPage.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { + Page, + PageSection, + PageSectionVariants, + TextVariants, + Title, + Text, + TextListVariants, + TextList, + TextListItem, + Divider, + Sidebar, + SidebarContent, + SidebarPanel, + Breadcrumb, + BreadcrumbItem, +} from '@patternfly/react-core'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import useSWRImmutable from 'swr/immutable'; +import { CSVToArray } from '@app/util'; +import { publicFetcher } from '@app/api'; +import PublicHeader from '@app/Header/PublicHeader'; +import Hero from '@app/components/Hero'; +import heroImg from '@app/bgimages/hero-img.jpeg'; +import EnterprisePremiumIcon from './EnterprisePremiumIcon'; + +import './support-page.css'; + +const SupportPage: React.FC = () => { + const { data } = useSWRImmutable('./public/incidents_technical_support.csv', publicFetcher); + const dataArr = CSVToArray(data); + function getHelpLink() { + return 'https://red.ht/open-support'; + } + + function createRowFromArr(dataLabel: string, label?: string) { + if (typeof label === 'undefined' || label === null) { + label = dataLabel; + } + return [label, ...dataArr.find((i) => i[0].startsWith(dataLabel))?.slice(1)]; + } + const types = createRowFromArr('Types', ''); + const columns = types; + const description = createRowFromArr('Description'); + const coverageHours = createRowFromArr('Hours of Coverage'); + const supportChannel = createRowFromArr('Support Channel'); + const responseTime = createRowFromArr('Response Time for technical support'); + const restorationTime = createRowFromArr('Restoration Time'); + const resolutionTime = createRowFromArr('Resolution Time'); + + const rows = [description, coverageHours, supportChannel, responseTime, restorationTime, resolutionTime]; + + return ( + }> + + + + <b>RHPDS Solution Support:</b> SLAs + + + + Home + + RHPDS Solution Support: SLAs + + + + +
+ + “We ran a pretty intense DevSecOps 1 day-workshop for one of our Premier Partners in Germany with 8 + attendees. The workshop required to have a cluster per attendee, which was a lot of hassle for us to set + up in the past. With RHPDS support we were able to focus on running the workshop and not on the + infrastructure. Highly appreciated!” + +
+

+ Goetz Rieger, +
Principal Solution Architect +

+
+
+ + + + Overview + + + RHPDS (Red Hat Product Demo System) is the platform which enables stakeholders, both internal and + external (specified Partners) with the ability to run demonstrations, hands-on workshops and personal + sandbox environments for definite time periods to showcase Red Hat’s portfolio of solutions. + + + + + Benefits + + + + Speedy restoration to get your business back up and running due to Red Hat’s fast response + time, in the event of a production-critical issue + + + Increased confidence because of restoration and resolution service-level agreements (SLAs) that + guarantee restoration and resolution + + + Efficient issue resolution with direct access to senior technical engineers who are familiar + with your environment + + + High availability of help through a designated support channel + + + + +
+ + + + + + {columns.map((column, columnIndex) => ( + + + {columnIndex === 3 ? ( + + ) : null} + {column} + + + ))} + + + + {rows.map((row, rowIndex) => ( + + {columns.map((_, columnIndex) => ( + + {row[columnIndex]} + + ))} + + ))} + + + + + Contact us + {' '} + for more information about RHPDS Solution Support. + + + + + Does not include external dependencies + + + Does not include Australia + + + Excluding regional holidays + + + + Excluded from Support are external dependencies and functionalities that enable the user to perform + modifications to the base environments, such as: LE, Quay, OpenShift Container Registry, Run as an OPEN + Environment, etc + + + + +
+
+ ); +}; + +export default SupportPage; diff --git a/catalog/ui/src/app/Support/support-page.css b/catalog/ui/src/app/Support/support-page.css new file mode 100644 index 000000000..590b4be08 --- /dev/null +++ b/catalog/ui/src/app/Support/support-page.css @@ -0,0 +1,9 @@ +.support-page { + padding: 0; +} +.support-page > * { + padding: 0 var(--pf-c-page__main-section--PaddingRight) 0 var(--pf-c-page__main-section--PaddingLeft); +} +.support-page > *:last-child { + padding-bottom: var(--pf-c-page__main-section--PaddingBottom); +} diff --git a/catalog/ui/src/app/Workshop/Workshop.tsx b/catalog/ui/src/app/Workshop/Workshop.tsx index e0079b3f4..fdc92dc91 100644 --- a/catalog/ui/src/app/Workshop/Workshop.tsx +++ b/catalog/ui/src/app/Workshop/Workshop.tsx @@ -4,7 +4,8 @@ import { Page } from '@patternfly/react-core'; import useSWRImmutable from 'swr/immutable'; import Footer from '@app/components/Footer'; import summitLogo from '@app/bgimages/Summit-Logo.svg'; -import { apiPaths, fetcher } from './workshop-utils'; +import { publicFetcher } from '@app/api'; +import { apiPaths } from './workshop-utils'; import { workshopLogin, WorkshopDetails } from './workshopApi'; import WorkshopAccess from './WorkshopAccess'; import WorkshopHeader from './WorkshopHeader'; @@ -19,7 +20,7 @@ const Workshop: React.FC = () => { const [loginFailureMessage, setLoginFailureMessage] = useState(''); const { data: workshop } = useSWRImmutable( workshopId ? apiPaths.WORKSHOP({ workshopId }) : null, - fetcher + publicFetcher ); const [workshopPrivateInfo, setWorkshopPrivateInfo] = useState(workshop); diff --git a/catalog/ui/src/app/Workshop/workshop-utils.ts b/catalog/ui/src/app/Workshop/workshop-utils.ts index 9a37ace68..9457ed8fa 100644 --- a/catalog/ui/src/app/Workshop/workshop-utils.ts +++ b/catalog/ui/src/app/Workshop/workshop-utils.ts @@ -1,14 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function fetcher(path: string, opt?: Record): Promise { - const response = await window.fetch(path, opt); - if (response.status >= 400 && response.status < 600) { - throw response; - } - const contentType = response.headers.get('Content-Type'); - if (contentType?.includes('text/')) return response.text(); - return response.json(); -} - export const apiPaths = { WORKSHOP: ({ workshopId }: { workshopId: string }): string => `/api/workshop/${workshopId}`, }; diff --git a/catalog/ui/src/app/api.ts b/catalog/ui/src/app/api.ts index 14ede9521..f37ccb098 100644 --- a/catalog/ui/src/app/api.ts +++ b/catalog/ui/src/app/api.ts @@ -130,11 +130,22 @@ export async function apiFetch(path: string, opt?: Record): Pro return resp; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function publicFetcher(path: string, opt?: Record): Promise { + const response = await window.fetch(path, opt); + if (response.status >= 400 && response.status < 600) { + throw response; + } + const contentType = response.headers.get('Content-Type'); + if (contentType?.includes('text/') || contentType?.includes('application/octet-stream')) return response.text(); + return response.json(); +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function fetcher(path: string, opt?: Record): Promise { const response = await apiFetch(path, opt); const contentType = response.headers.get('Content-Type'); - if (contentType?.includes('text/')) return response.text(); + if (contentType?.includes('text/') || contentType?.includes('application/octet-stream')) return response.text(); return response.json(); } diff --git a/catalog/ui/src/app/bgimages/hero-img.jpeg b/catalog/ui/src/app/bgimages/hero-img.jpeg new file mode 100644 index 000000000..d13fa9512 Binary files /dev/null and b/catalog/ui/src/app/bgimages/hero-img.jpeg differ diff --git a/catalog/ui/src/app/components/Hero.tsx b/catalog/ui/src/app/components/Hero.tsx new file mode 100644 index 000000000..4513aec78 --- /dev/null +++ b/catalog/ui/src/app/components/Hero.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const Hero: React.FC<{ image: string; children: React.ReactNode }> = ({ image, children, ...rest }) => { + const style: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyItems: 'center', + color: '#fff', + textAlign: 'center', + backgroundImage: `url(${image})`, + backgroundSize: 'cover', + backgroundPositionY: 'center', + padding: '128px 24px', + margin: 0, + marginBottom: 'var(--pf-global--spacer--md)', + }; + return ( +
+ {children} +
+ ); +}; + +export default Hero; diff --git a/catalog/ui/src/app/components/TermsOfService.tsx b/catalog/ui/src/app/components/TermsOfService.tsx index a3ce5dfa4..d2eb27bb4 100644 --- a/catalog/ui/src/app/components/TermsOfService.tsx +++ b/catalog/ui/src/app/components/TermsOfService.tsx @@ -1,18 +1,14 @@ -import * as React from 'react'; - +import React from 'react'; import { Checkbox, FormGroup } from '@patternfly/react-core'; - import { renderContent } from '@app/util'; -export interface TermsOfServiceProps { +const TermsOfService: React.FC<{ agreed: boolean; onChange: (checked: boolean, event: React.FormEvent) => void; text?: string; -} - -const TermsOfService: React.FunctionComponent = ({ agreed, onChange, text }) => { +}> = ({ agreed, onChange, text }) => { return ( - +
import('@app/Catalog/CatalogItemForm')) const Services = React.lazy(() => import('@app/Services/Services')); const Workshops = React.lazy(() => import('@app/Workshops/Workshops')); const Workshop = React.lazy(() => import('@app/Workshop/Workshop')); +const SupportPage = React.lazy(() => import('@app/Support/SupportPage')); const NotFound = React.lazy(() => import('@app/NotFound/NotFound')); const AnarchyActionInstance = React.lazy(() => import('@app/Admin/AnarchyActionInstance')); const AnarchyActions = React.lazy(() => import('@app/Admin/AnarchyActions')); @@ -253,6 +254,11 @@ const publicRoutes: IAppRoute[] = [ path: '/workshop/:workshopId', title: 'Workshop | Babylon', }, + { + component: SupportPage, + path: '/support', + title: 'Support | Babylon', + }, ]; const _Routes: React.FC = () => { diff --git a/catalog/ui/src/public/incidents_technical_support.csv b/catalog/ui/src/public/incidents_technical_support.csv new file mode 100644 index 000000000..0e4784814 --- /dev/null +++ b/catalog/ui/src/public/incidents_technical_support.csv @@ -0,0 +1,20 @@ +"Types (Will appear in the UI next to each CI, and will have a new web page for it)",Community Content,Enterprise- Standard,Enterprise- Premium,Pre Scheduled Events (min 2 weeks) +Description,Assets Created by TMM and Tech Sales,Open Environments & LPE by RHPDS,Currated Assets by RHPDS (like Epic Demo) and promoted CIs (from community to enterprise),"Customer workshops, Summit etc.." +User prerequisites,,,,at least 2 weeks notice via ticket to pre provision and allocate resources +Hours of Coverage,Mon-Fri during business hours,Mon-Fri during business hours,Mon-Fri during business hours,Mon-Fri during business hours +"Support Channel (internals, partners, customers?)",Ticketing system & Chat channel with Developers,Ticketing System & Chat,Ticketing System & Chat,Ticketing System and dedicated Chat space +Optional,,,,Dedicated Technical Support Liason: Remote or On site +Response Time for technical support,10 business hours,10 business hours,10 business hours,30 minutes +Restoration Time (workaround or plan B),Best effort,5 business days,2 business days,1 business hour +Resolution Time for technical issues,20 business days,10 business days,5 business days,1 business day +Uptime (excluding external dependencies and maintenance windows),Best effort,,, +Pools,Min 1 per CI,Min 2 per CI,Min 5 per CI, +Using AgD and AgV,Mandatory,Mandatory,Mandatory, +Can come back from Idle?,Mandatory,Mandatory,Mandatory, +QA automation for Deployment,Mandatory,Mandatory,Mandatory, +QA automation for Content,Optional,,Mandatory, +,,,, +,,,, +,,Does not include external dependencies,, +,,Does not include Australia,, +,,,, \ No newline at end of file diff --git a/catalog/ui/webpack.common.js b/catalog/ui/webpack.common.js index 61edb6d0f..0df435844 100644 --- a/catalog/ui/webpack.common.js +++ b/catalog/ui/webpack.common.js @@ -81,6 +81,7 @@ module.exports = () => { filename: '[name].[contenthash:8].bundle.js', chunkFilename: '[name].[contenthash:8].bundle.js', publicPath: ASSET_PATH, + assetModuleFilename: 'fonts/[hash:8][ext][query]', }, plugins: [ new Dotenv({ @@ -94,7 +95,7 @@ module.exports = () => { new CopyPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/favicon.ico'), to: 'images' }, - { from: path.resolve(__dirname, 'src/public'), to: '' }, + { from: path.resolve(__dirname, 'src/public'), to: 'public' }, ], }), ],