diff --git a/CHANGELOG.md b/CHANGELOG.md index bb800f557..a0759af1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,24 @@ reference the gitlab/github issue that is related to the change. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [v10.5.0] - TBD +## [v10.6.0] - TBD - `#47` Fix subscription dropdowns to not resize when typing and fixes alignments with other input fields - Added description text in Owner_Subscription_Id and added a copy-to-clipboard button -- Updated service ticket detail page to show "last_update_time" - `#41` Converted most components to a Functions based approach - Selecting "auto scroll" on process detail page is now stored in localstorage - `#44` Include subscription description of first product block relation on Subscription detail page + +Surf specific: + +- Added a log viewer to Service ticket detail +- Refactored Impacted objects tables to also show IMS Circuits +- Fix CIM impacted objects without a subscription ID +- Updated service ticket detail page to show "last_update_time" - Added button to restart CIM open relate step + +## [v10.5.0] - 2022-10-24 + - `#42` Fix help button hidden on subscriptions page - `#141` Convert sass files to emotion 3 - `#157` Redirect workflow submit to process detail diff --git a/README.md b/README.md index 2e6d8fab7..fa1aa322f 100644 --- a/README.md +++ b/README.md @@ -143,11 +143,11 @@ seems to fix that issue, whilst keeping hot reload intact. So try adding this to To run the storybook, do: `yarn storybook`. Your browser should open to http://localhost:9009/?path=/story/welcome--to-storybook. - ## Actions and page views depending on who you are. + The orchestrator client can be configured to allow access to a page or to buttons in the client. The react app consumes a webassembly OPA policy and evaluates if the user has the correct claims to view certain resources. -The function that gets called is the ```allowed()``` function in `src/utils/policy.ts`. The implementation of `allowed()` +The function that gets called is the `allowed()` function in `src/utils/policy.ts`. The implementation of `allowed()` is done as follows: ```typescript jsx @@ -163,58 +163,54 @@ Basically it boils down to: when a call to `allowed` returns true the component **When no policy is found, the orchestrator-client will allow access to the resource. Any real access must be enforced by the API. The client only disables features with the `allowed` function** - ### Pages These are the pages in the orchestrator client and how the resources can be viewed. In the implementation we only disable menu items, not the actual pages. If a user has a direct url they will still be able to access the resource. -|Page|Resource name| -|---|---| -|Processes Pages|`/orchestrator/processes/`| -|Subscriptions Pages|`/orchestrator/subscriptions/`| -|Metadata Pages|`/orchestrator/metadata/`| -|Tasks Pages|`/orchestrator/tasks/`| -|Settings Pages|`/orchestrator/settings/`| +| Page | Resource name | +| ------------------- | ------------------------------ | +| Processes Pages | `/orchestrator/processes/` | +| Subscriptions Pages | `/orchestrator/subscriptions/` | +| Metadata Pages | `/orchestrator/metadata/` | +| Tasks Pages | `/orchestrator/tasks/` | +| Settings Pages | `/orchestrator/settings/` | If you would like to add an extra menu item you are free to name it as you wish. It is defined in the `allowed` function. ### Actions + Actions are disabled in the same manner as menu items. The following actions are configurable: -|Action|Location|Resource name| Explanation| -|---|---|---|---| -|Modify a subscription|Subscription detail page action menu|`/orchestrator/subscriptions/modify/*`|This resource can be configured per workflow| -|Terminate a subscription| " "|`/orchestrator/subscriptions/terminate/*`|With this resource you can terminate a subscription| -|Validate a subscription|" "|`/orchestrator/subscriptions/validate/*`|With this resource you can validate a certain subscription| -|View a subscription from the process detail page|The process detail page|`/orchestrator/subscriptions/view/from-process`|Interact with a subscription from the process detail page| -|Abort a process|" " |`/orchestrator/processes/abort/*`| The ability to abort a process| -|Delete a process|" "|`/orchestrator/processes/delete/*`| The ability to delete a process, this is always disabled for processes not for tasks| -|Retry a process|" "|`/orchestrator/processes/retry/*`| The ability to retry a failed process or task| -|View a subscription from a process|" "|`/orchestrator/subscriptons/view/from-process` |This enables the link towards the subscription detail page| -|View a process detail page| The process list page| `/orchestrator/processes/details/*`| The allows the person to visit a process detail page| -|Retry all tasks| The tasks list page|`/orchestrator/processes/all-tasks/retry`| The Retry all tasks button| -|Create a task| The tasks list page|`/orchestrator/processes/create/task`| Create a task| -|Create a new subscription|The new process button|`/orchestrator/processes/create/process/menu`|| -|Render a user_input_form|Allow access to input steps| `/orchestrator/processes/user_inout/*`|Allow access to input_steps| -|Allow deletion of product blocks| Product block detail page|`/orchestrator/metadata/product-block/delete/*`|| -|Edit a product block| " "|`/orchestrator/metadata/product-block/edit/*`|| -|View a product block|Product block list page|`/orchestrator/metadata/prodcut-block/view/*`|| -|View a product|Product list page|`/orchestrator/metadata/product/view/*`|| -|Edit a product|Product detail page|`/orchestrator/metadata/product/edit/*`|| -|Delete a product|Product detail page|`/orchestrator/metadata/product/delete/*`|| +| Action | Location | Resource name | Explanation | +| ------------------------------------------------ | ------------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------ | +| Modify a subscription | Subscription detail page action menu | `/orchestrator/subscriptions/modify/*` | This resource can be configured per workflow | +| Terminate a subscription | " " | `/orchestrator/subscriptions/terminate/*` | With this resource you can terminate a subscription | +| Validate a subscription | " " | `/orchestrator/subscriptions/validate/*` | With this resource you can validate a certain subscription | +| View a subscription from the process detail page | The process detail page | `/orchestrator/subscriptions/view/from-process` | Interact with a subscription from the process detail page | +| Abort a process | " " | `/orchestrator/processes/abort/*` | The ability to abort a process | +| Delete a process | " " | `/orchestrator/processes/delete/*` | The ability to delete a process, this is always disabled for processes not for tasks | +| Retry a process | " " | `/orchestrator/processes/retry/*` | The ability to retry a failed process or task | +| View a subscription from a process | " " | `/orchestrator/subscriptons/view/from-process` | This enables the link towards the subscription detail page | +| View a process detail page | The process list page | `/orchestrator/processes/details/*` | The allows the person to visit a process detail page | +| Retry all tasks | The tasks list page | `/orchestrator/processes/all-tasks/retry` | The Retry all tasks button | +| Create a task | The tasks list page | `/orchestrator/processes/create/task` | Create a task | +| Create a new subscription | The new process button | `/orchestrator/processes/create/process/menu` | | +| Render a user_input_form | Allow access to input steps | `/orchestrator/processes/user_inout/*` | Allow access to input_steps | +| Allow deletion of product blocks | Product block detail page | `/orchestrator/metadata/product-block/delete/*` | | +| Edit a product block | " " | `/orchestrator/metadata/product-block/edit/*` | | +| View a product block | Product block list page | `/orchestrator/metadata/prodcut-block/view/*` | | +| View a product | Product list page | `/orchestrator/metadata/product/view/*` | | +| Edit a product | Product detail page | `/orchestrator/metadata/product/edit/*` | | +| Delete a product | Product detail page | `/orchestrator/metadata/product/delete/*` | | New actions or other actions can be enabled or disabled in the same way as menu items, by adding an arbitrary resource to the project. - ## Development tips ### IDE plugins - **Visual Studio Code** -* [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) for syntax highlighting `emotion/css` - - +- [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=styled-components.vscode-styled-components) for syntax highlighting `emotion/css` diff --git a/package.json b/package.json index 8392925ab..9575a0451 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "workflow-client", - "version": "1.0.0", + "version": "10.6.0", "private": false, "license": "Apache-2.0", "proxy": "http://localhost:8080", @@ -15,8 +15,8 @@ "storybook": "start-storybook -p 9009 -s ./public", "storybook-build": "build-storybook -s ./public", "storybook-deploy": "PUBLIC_URL=https://workfloworchestrator.github.io/orchestrator-core-gui gh-pages -d storybook-static", - "prettier": "prettier -c \"{**/*.{js,jsx,scss,md,ts,tsx,json},public/**/*.html}\"", - "prettier-fix": "prettier --write \"{**/*.{js,jsx,scss,md,ts,tsx,json},public/**/*.html}\"", + "prettier": "prettier -c \"{**/*.{js,jsx,scss,md,ts,tsx,json},public/**/*.html,*.md}\"", + "prettier-fix": "prettier --write \"{**/*.{js,jsx,scss,md,ts,tsx,json},public/**/*.html,*.md}\"", "extract": "formatjs extract", "compile": "formatjs compile" }, diff --git a/src/custom-surf/api/index.ts b/src/custom-surf/api/index.ts index 59f6db36b..b191b70fa 100644 --- a/src/custom-surf/api/index.ts +++ b/src/custom-surf/api/index.ts @@ -3,7 +3,6 @@ import { OpenServiceTicketPayload, ServiceTicket, ServiceTicketBackgroundJobCount, - ServiceTicketImpactedIMSCircuit, ServiceTicketProcessState, ServiceTicketWithDetails, } from "custom/types"; @@ -72,9 +71,8 @@ abstract class CustomApiClientInterface extends BaseApiClient { abstract cimTicketById: (ticket_id: string) => Promise; abstract cimPatchImpactedObject: ( ticket_id: string, - subscription_id: string, - circuit_id: number, - impactedObject: ServiceTicketImpactedIMSCircuit + index: number, + impact: string ) => Promise; } @@ -280,15 +278,10 @@ export class CustomApiClient extends CustomApiClientInterface { ); }; - cimPatchImpactedObject = ( - ticket_id: string, - subscription_id: string, - circuit_id: number, - impactedObject: ServiceTicketImpactedIMSCircuit - ): Promise => { + cimPatchImpactedObject = (ticket_id: string, index: number, impact: string): Promise => { return this.postPutJson( - prefix_cim_dev_uri(`surf/cim/objects/${ticket_id}/subscription/${subscription_id}/circuit/${circuit_id}`), - impactedObject, + prefix_cim_dev_uri(`surf/cim/objects/${ticket_id}/${index}`), + { impact_override: impact }, "patch", true, false diff --git a/src/custom-surf/components/cim/BackgroundJobLogs.tsx b/src/custom-surf/components/cim/BackgroundJobLogs.tsx new file mode 100644 index 000000000..08d4f95c5 --- /dev/null +++ b/src/custom-surf/components/cim/BackgroundJobLogs.tsx @@ -0,0 +1,59 @@ +import { EuiBasicTable } from "@elastic/eui"; +import { BackgroundJobLog } from "custom/types"; +import { renderStringAsDateTime } from "custom/Utils"; +import React, { Fragment } from "react"; +import { WrappedComponentProps, injectIntl } from "react-intl"; + +interface IProps extends WrappedComponentProps { + data: BackgroundJobLog[]; +} + +const BackgroundJobLogs = ({ data }: IProps) => { + let columns = [ + { + field: "entry_time", + name: "Date", + render: (entry_time: string, data: any) => renderStringAsDateTime(data.entry_time), + width: 200, + schema: "date", + }, + { + field: "process_state", + name: "State", + truncateText: true, + sortable: true, + width: 100, + }, + { + field: "message", + name: "Log message", + sortable: true, + truncateText: true, + width: "40%", + }, + { + field: "subscription_id", + name: "Subscription ID", + sortable: true, + width: 200, + }, + { + field: "customer ID", + name: "Customer ID", + sortable: true, + width: 200, + }, + ]; + + return ( + + + + ); +}; +export default injectIntl(BackgroundJobLogs); diff --git a/src/custom-surf/components/cim/ImpactedObjects.tsx b/src/custom-surf/components/cim/ImpactedObjects.tsx new file mode 100644 index 000000000..fb3e363af --- /dev/null +++ b/src/custom-surf/components/cim/ImpactedObjects.tsx @@ -0,0 +1,300 @@ +import { + EuiBadge, + EuiBasicTable, + EuiButton, + EuiButtonIcon, + EuiFlexGrid, + EuiFlexItem, + EuiForm, + EuiLink, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiScreenReaderOnly, + EuiTitle, + RIGHT_ALIGNMENT, +} from "@elastic/eui"; +import { HorizontalAlignment } from "@elastic/eui/src/services/alignment"; +import ImsCircuitInfo from "custom/components/cim/ImsCiruitInfo"; +import { + ImpactedObject, + ServiceTicketImpactedObject, + ServiceTicketImpactedObjectImpact, + ServiceTicketWithDetails, +} from "custom/types"; +import { isDate } from "lodash"; +import React, { Fragment, ReactNode, useContext, useState } from "react"; +import { WrappedComponentProps, injectIntl } from "react-intl"; +import ReactSelect from "react-select"; +import { getReactSelectTheme } from "stylesheets/emotion/utils"; +import ApplicationContext from "utils/ApplicationContext"; +import { Option } from "utils/types"; +import { isEmpty } from "utils/Utils"; + +interface IProps extends WrappedComponentProps { + ticket: ServiceTicketWithDetails; + updateable: boolean; + withSubscriptions: boolean; +} + +const options: Option[] = (Object.values(ServiceTicketImpactedObjectImpact) as string[]).map((val) => ({ + value: val, + label: val, +})); + +const getFilteredImpactedObjects = (impactedObjects: ServiceTicketImpactedObject[], withSubscriptions: boolean) => { + let result: ImpactedObject[] = []; + for (const [index, impactedObject] of impactedObjects.entries()) { + if (!withSubscriptions && impactedObject.subscription_id) { + continue; + } + if (withSubscriptions && !impactedObject.subscription_id) { + continue; + } + + const tempImpactedCircuit: ImpactedObject = { + id: `item-${index}`, + customer: impactedObject.owner_customer.customer_name, + impact: impactedObject.ims_circuits[0].impact, + type: impactedObject.product_type, + subscription: impactedObject.subscription_description, + impact_override: impactedObject.impact_override, + subscription_id: impactedObject.subscription_id, + ims_info: impactedObject.ims_circuits, + }; + result.push(tempImpactedCircuit); + } + return result; +}; + +const ImpactedObjects = ({ ticket, updateable, withSubscriptions }: IProps) => { + const { theme, customApiClient } = useContext(ApplicationContext); + const [isModalVisible, setIsModalVisible] = useState(false); + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); + const [editImpactedObject, setEditImpactedObject] = useState(); + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{ [key: string]: ReactNode }>({}); + const customStyles = getReactSelectTheme(theme); + + const data = getFilteredImpactedObjects(ticket.impacted_objects, withSubscriptions); + + const [items, setItems] = useState(data); + + let allExpandedRows: { [key: string]: ReactNode } = {}; + for (const item of data) { + allExpandedRows[item.id] = ; + } + + const toggleExpandAll = () => { + if (!isEmpty(itemIdToExpandedRowMap)) { + setItemIdToExpandedRowMap({}); + } else { + setItemIdToExpandedRowMap(allExpandedRows); + } + }; + + const toggleDetails = (item: ImpactedObject) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMapValues[item.id]) { + delete itemIdToExpandedRowMapValues[item.id]; + } else { + itemIdToExpandedRowMapValues[item.id] = ; + } + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }; + + const removeEdit = (): void => { + setEditImpactedObject(undefined); + }; + + const onChangeImpactOverride = (impactedObject: ImpactedObject) => async (e: any): Promise => { + let value: any; + if (isEmpty(e) || isDate(e)) { + value = e; + } else { + value = e.target ? e.target.value : e.value; + } + let index = Number(impactedObject.id.replace("item-", "")); + + removeEdit(); + let updatedImpactedObject: ImpactedObject = { ...impactedObject, impact_override: value }; + await submitImpactOverride(index, value); + setItems(items.map((obj: ImpactedObject) => (obj.id === impactedObject.id ? updatedImpactedObject : obj))); + closeModal(); + }; + + const submitImpactOverride = async (index: number, impact: string): Promise => { + await customApiClient.cimPatchImpactedObject(ticket._id, index, impact); + }; + + const showImpact = (impact: ImpactedObject): any => { + return ( + + + className="impact-override__select" + onChange={onChangeImpactOverride(impact)} + options={options} + isSearchable={false} + value={options.filter( + (option) => impact?.impact_override && option["value"] === impact.impact_override + )} + isClearable={true} + defaultMenuIsOpen={true} + autoFocus + styles={customStyles} + /> + + ); + }; + + const align_right: HorizontalAlignment = RIGHT_ALIGNMENT; + let columns = [ + { + field: "customer", + name: "Customer", + sortable: true, + truncateText: true, + width: "20%", + }, + { + field: "subscription", + name: "Subscription", + sortable: true, + render: (name: string, record: any) => + record.subscription_id ? ( + + {name} + + ) : ( + "" + ), + width: "30%", + }, + { + field: "type", + name: "Product type", + truncateText: true, + width: "100", + }, + { + field: "id", + name: "Affected services", + render: (id: string, record: any) => {record.ims_info.length}, + width: "150", + }, + { + field: "impact", + name: "Impact", + truncateText: true, + width: "120", + }, + { + name: "Impact override", + field: "impact_override", + render: (id: string, record: any) => (record?.impact_override ? record.impact_override : "-"), + width: "120", + }, + { + name: "", + field: "edit_impact_override", + actions: [ + { + name: "Edit impact", + description: "Override the impact level", + type: "icon", + icon: "pencil", + onClick: (record: any) => { + setEditImpactedObject(record); + showModal(); + }, + }, + ], + width: "30", + }, + { + align: align_right, + width: "40px", + isExpander: true, + name: ( + + Expand rows + + ), + render: (item: ImpactedObject) => ( + toggleDetails(item)} + aria-label={itemIdToExpandedRowMap[item.id] ? "Collapse" : "Expand"} + iconType={itemIdToExpandedRowMap[item.id] ? "arrowUp" : "arrowDown"} + /> + ), + }, + ]; + + // Hide subscription and other columns when not needed + if (!withSubscriptions) { + columns = columns.filter( + (c) => + c.field !== "subscription" && + c.field !== "type" && + c.field !== "impact_override" && + c.field !== "edit_impact_override" + ); + } + + // Hide impact override when needed + if (!updateable) { + columns = columns.filter((c) => c.field !== "edit_impact_override"); + } + + return ( + + {isModalVisible && ( + + + +

Override impact

+
+
+ {editImpactedObject && showImpact(editImpactedObject)} + + + Close + + +
+ )} + + + +

+ {withSubscriptions ? "Impact on subscriptions" : "Impact on services without subscriptions"} +

+
+
+ + + +
+ + +
+ ); +}; +export default injectIntl(ImpactedObjects); diff --git a/src/custom-surf/components/cim/ServiceTicketDetailImpactedObjectsStyling.ts b/src/custom-surf/components/cim/ImsCircuitInfoStyling.ts similarity index 63% rename from src/custom-surf/components/cim/ServiceTicketDetailImpactedObjectsStyling.ts rename to src/custom-surf/components/cim/ImsCircuitInfoStyling.ts index 90b4fdda7..c106dcbca 100644 --- a/src/custom-surf/components/cim/ServiceTicketDetailImpactedObjectsStyling.ts +++ b/src/custom-surf/components/cim/ImsCircuitInfoStyling.ts @@ -3,12 +3,10 @@ import { DARKEST_PRIMARY_COLOR, DARK_GOLD_COlOR, DARK_GREY_COLOR, - DARK_SUCCESS_COLOR, LIGHTER_GREY_COLOR, LIGHT_GOLD_COLOR, LIGHT_GREY_COLOR, LIGHT_PRIMARY_COLOR, - LIGHT_SUCCESS_COLOR, MEDIUM_GREY_COLOR, } from "stylesheets/emotion/colors"; import { phoneMediaQuery } from "stylesheets/emotion/mediaQueries"; @@ -34,17 +32,16 @@ const table_phone = css` float: left; text-transform: uppercase; font-weight: bold; - font-size: 14px; + font-size: 12px; color: ${MEDIUM_GREY_COLOR}; } } `; -export const tableImpactedObjects = css` - table.ticket-impacted-objects { +export const tableImsCircuitInfo = css` + table.ims-circuit-info { width: 100%; word-break: break-all; - margin: 10px; td, th { @@ -53,47 +50,25 @@ export const tableImpactedObjects = css` } tr { - cursor: pointer; - - &.light { - border-bottom: 1px solid ${LIGHT_GREY_COLOR}; - } - - &.dark { - border-bottom: 1px solid ${DARK_GREY_COLOR}; - } - - &:hover { - &.light { - background-color: ${shadeColor(LIGHT_PRIMARY_COLOR, -10)}; - } - &.dark { - background-color: ${shadeColor(DARKEST_PRIMARY_COLOR, -10)}; - } - } - } - - tr.Allocated { &.light { - background-color: ${LIGHT_PRIMARY_COLOR}; + border-top: 1px solid ${LIGHT_GREY_COLOR}; } &.dark { - background-color: ${DARKEST_PRIMARY_COLOR}; + border-top: 1px solid ${DARK_GREY_COLOR}; } &:hover { &.light { background-color: ${shadeColor(LIGHT_PRIMARY_COLOR, -10)}; } - &.dark { background-color: ${shadeColor(DARKEST_PRIMARY_COLOR, -10)}; } } } - tr.Planned { + tr.down { /* cursor: default; */ &.light { @@ -115,66 +90,41 @@ export const tableImpactedObjects = css` } } - tr.Free { - &.light { - background-color: ${LIGHT_SUCCESS_COLOR}; - } - - &.dark { - background-color: ${DARK_SUCCESS_COLOR}; - } - - &:hover { - &.light { - background-color: ${shadeColor(LIGHT_SUCCESS_COLOR, -10)}; - } - - &.dark { - background-color: ${shadeColor(DARK_SUCCESS_COLOR, -10)}; - } - } - } - th { - padding: 5px 5px 10px 5px; + padding: 0 5px 0 5px; } th.customer { width: 15%; } - - td.impact-override { - padding: 0 0 0 5px; - - & div.impact-override__select { - max-width: 200px; - } - - & div.impact-override__text { - width: 170px; - } + th.ims_circuit_id { + width: 100px; + } + td.ims-info { + width: 40%; } span { text-transform: uppercase; font-weight: bold; - font-size: larger; color: ${MEDIUM_GREY_COLOR}; - padding: 7px 2px 7px 0; + padding: 7px 2px 0 0; } i.fa { float: right; margin-right: 15px; color: ${MEDIUM_GREY_COLOR}; - font-size: 18px; + font-size: 15px; } tbody { td { word-break: break-word; word-wrap: break-word; - padding: 15px 0 10px 5px; + padding: 5px 0 10px 5px; + vertical-align: top; + font-size: 12px; div.tool-tip { span { diff --git a/src/custom-surf/components/cim/ImsCiruitInfo.tsx b/src/custom-surf/components/cim/ImsCiruitInfo.tsx new file mode 100644 index 000000000..0a79884e7 --- /dev/null +++ b/src/custom-surf/components/cim/ImsCiruitInfo.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2022 SURF. + * 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 { EuiLink, EuiPanel } from "@elastic/eui"; +import { tableImsCircuitInfo } from "custom/components/cim/ImsCircuitInfoStyling"; +import { ImsInfo } from "custom/types"; +import { ENV } from "env"; +import React, { useContext } from "react"; +import { FormattedMessage, WrappedComponentProps, injectIntl } from "react-intl"; +import ApplicationContext from "utils/ApplicationContext"; + +type Column = "ims_circuit_id" | "ims_circuit_name" | "extra_info" | "impact"; + +interface IProps extends WrappedComponentProps { + imsInfo: ImsInfo[]; +} +const columns: Column[] = ["ims_circuit_id", "ims_circuit_name", "extra_info", "impact"]; + +const ImsCircuitInfo = ({ imsInfo }: IProps) => { + const { theme } = useContext(ApplicationContext); + + const th = (name: Column, index: number) => { + return ( + + + + + + ); + }; + + const createRow = (item: ImsInfo) => { + return ( + + + + {item.ims_circuit_id} + + + {item.ims_circuit_name} + {item.extra_information} + {item.impact} + + ); + }; + + return ( + + + + {columns.map((column, index) => th(column, index))} + + {imsInfo.map(createRow)} +
+
+ ); +}; + +export default injectIntl(ImsCircuitInfo); diff --git a/src/custom-surf/components/cim/ServiceTicketDetailImpactedObjects.tsx b/src/custom-surf/components/cim/ServiceTicketDetailImpactedObjects.tsx deleted file mode 100644 index d150beb0f..000000000 --- a/src/custom-surf/components/cim/ServiceTicketDetailImpactedObjects.tsx +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2019-2022 SURF. - * 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 { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from "@elastic/eui"; -import { tableImpactedObjects } from "custom/components/cim/ServiceTicketDetailImpactedObjectsStyling"; -import { ServiceTicketImpactedObjectImpact, ServiceTicketWithDetails } from "custom/types"; -import { isDate, isEmpty } from "lodash"; -import React, { useContext, useEffect, useState } from "react"; -import { FormattedMessage, WrappedComponentProps, injectIntl } from "react-intl"; -import Select from "react-select"; -import ApplicationContext from "utils/ApplicationContext"; -import { Option, SortOption, SubscriptionModel } from "utils/types"; -import { stop } from "utils/Utils"; - -type Column = "customer" | "impact" | "type" | "subscription" | "impact_override"; - -interface IProps extends WrappedComponentProps { - ticket: ServiceTicketWithDetails; - updateable: boolean; - acceptImpactedObjects: () => void; -} - -export interface ImpactedObject { - customer: string; - impact: ServiceTicketImpactedObjectImpact; - type: string; - subscription: string; - impact_override?: ServiceTicketImpactedObjectImpact; - subscription_id: string; - ims_circuit_id: number; - ims_circuit_name: string; - extra_information?: string; -} - -const options: Option[] = (Object.values(ServiceTicketImpactedObjectImpact) as string[]).map((val) => ({ - value: val, - label: val, -})); - -const columns: Column[] = ["customer", "impact", "type", "subscription", "impact_override"]; - -const sortBy = (name: Column) => (a: ImpactedObject, b: ImpactedObject) => { - const aSafe = a[name] || ""; - const bSafe = b[name] || ""; - return typeof aSafe === "string" || typeof bSafe === "string" - ? (aSafe as string).toLowerCase().localeCompare(bSafe.toString().toLowerCase()) - : aSafe - bSafe; -}; - -const sortColumnIcon = (name: string, sorted: SortOption) => { - if (sorted.name === name) { - return ; - } - return ; -}; - -const ServiceTicketDetailImpactedObjects = ({ ticket, updateable, acceptImpactedObjects }: IProps) => { - const { theme, customApiClient, apiClient } = useContext(ApplicationContext); - const [impactedObjects, setImpactedObjects] = useState>([]); - const [editImpactedObject, setEditImpactedObject] = useState(); - const [sortOrder, setSortOrder] = useState>({ name: "subscription", descending: false }); - - const getImpactedObjects = async () => { - const subscriptions: Record = {}; - - // Retrieve each subscription once - for (const impacted of ticket.impacted_objects) { - if (impacted.subscription_id in subscriptions || !impacted.subscription_id) { - continue; - } - subscriptions[impacted.subscription_id] = await apiClient.subscriptionsDetailWithModel( - impacted.subscription_id - ); - } - - const newImpactedObjects: ImpactedObject[] = ticket.impacted_objects - .map((impactedObject) => { - let subscription = subscriptions[impactedObject.subscription_id]; - if (!subscription) { - return []; - } - - return impactedObject.ims_circuits.map((imsCircuit) => { - return { - customer: impactedObject.owner_customer.customer_name, - impact: imsCircuit.impact, - type: subscription.product.product_type, - subscription: subscription.description, - impact_override: imsCircuit.impact_override, - subscription_id: impactedObject.subscription_id, - ims_circuit_id: imsCircuit.ims_circuit_id, - ims_circuit_name: imsCircuit.ims_circuit_name, - extra_information: imsCircuit.extra_information, - }; - }); - }) - .reduce((a, b) => a.concat(b), []); - setImpactedObjects(newImpactedObjects); - }; - - const toggleSort = (name: Column) => (e: React.MouseEvent) => { - stop(e); - const newSortOrder = { ...sortOrder }; - newSortOrder.descending = newSortOrder.name === name ? !newSortOrder.descending : false; - newSortOrder.name = name; - setSortOrder(newSortOrder); - }; - - const sort = (unsorted: ImpactedObject[]) => { - const { name, descending } = sortOrder; - const sorted = unsorted.sort(sortBy(name)); - if (descending) { - sorted.reverse(); - } - return sorted; - }; - - const sumbitImpactOverride = async (impactedObject: ImpactedObject): Promise => { - await customApiClient.cimPatchImpactedObject( - ticket._id, - impactedObject.subscription_id, - impactedObject.ims_circuit_id, - impactedObject - ); - }; - - const editImpact = (impactedObject: ImpactedObject): (() => void) => { - return () => updateable && setEditImpactedObject(impactedObject); - }; - - const removeEdit = (): void => { - setEditImpactedObject(undefined); - }; - - const onChangeImpactOverride = (impactedObject: ImpactedObject) => async (e: any): Promise => { - let value: any; - if (isEmpty(e) || isDate(e)) { - value = e; - } else { - // @ts-ignore - value = e.target ? e.target.value : e.value; - } - removeEdit(); - let updatedImpactedObject: ImpactedObject = { ...impactedObject, impact_override: value }; - await sumbitImpactOverride(updatedImpactedObject); - setImpactedObjects( - impactedObjects.map((obj: ImpactedObject) => - obj.ims_circuit_id === impactedObject.ims_circuit_id ? updatedImpactedObject : obj - ) - ); - }; - - const showImpact = (impact: ImpactedObject): any => { - if (impact === editImpactedObject) { - return ( - - className="impact-override__select" - onChange={onChangeImpactOverride(impact)} - onBlur={removeEdit} - options={options} - isSearchable={false} - value={options.filter( - (option) => impact.impact_override && option["value"] === impact.impact_override - )} - isClearable={true} - defaultMenuIsOpen={true} - autoFocus - /> - ); - } else if (updateable) { - return ( - <> - - - {impact.impact_override && {impact.impact_override}} - - - - - ); - } - return impact.impact_override || "-"; - }; - - const th = (name: Column, index: number) => { - return ( - - - - - {sortColumnIcon(name, sortOrder)} - - ); - }; - - const createImpactObjectValueRow = (item: ImpactedObject) => { - return ( - - {item.customer} - {item.impact} - {item.type} - {item.subscription} - {showImpact(item)} - - ); - }; - - useEffect(() => { - getImpactedObjects(); - }, [ticket]); // eslint-disable-line react-hooks/exhaustive-deps - - return ( - <> - - - -

Impacted objects

-
-
-
- - - - {columns.map((column, index) => th(column, index))} - - {sort(impactedObjects).map(createImpactObjectValueRow)} -
-
- - - - - - - - - - ); -}; - -export default injectIntl(ServiceTicketDetailImpactedObjects); diff --git a/src/custom-surf/manifest.json b/src/custom-surf/manifest.json index c2d52d052..ec15537b5 100644 --- a/src/custom-surf/manifest.json +++ b/src/custom-surf/manifest.json @@ -13,7 +13,7 @@ "path": "pages", "file": "ServiceTickets", "component": "ServiceTickets", - "showInMenu": false + "showInMenu": true }, { "name": "tickets/create", diff --git a/src/custom-surf/pages/ServiceTicketDetail.tsx b/src/custom-surf/pages/ServiceTicketDetail.tsx index b55f575e9..06a819706 100644 --- a/src/custom-surf/pages/ServiceTicketDetail.tsx +++ b/src/custom-surf/pages/ServiceTicketDetail.tsx @@ -15,10 +15,16 @@ import { EuiButton, + EuiButtonIcon, EuiFacetButton, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, EuiPage, EuiPageBody, EuiPanel, @@ -26,7 +32,8 @@ import { EuiTitle, } from "@elastic/eui"; import { TabbedSection } from "components/subscriptionDetail/TabbedSection"; -import ServiceTicketDetailImpactedObjects from "custom/components/cim/ServiceTicketDetailImpactedObjects"; +import BackgroundJobLogs from "custom/components/cim/BackgroundJobLogs"; +import ImpactedObjects from "custom/components/cim/ImpactedObjects"; import { ticketDetail } from "custom/pages/ServiceTicketDetailStyling"; import { ServiceTicketLog, @@ -36,15 +43,14 @@ import { } from "custom/types"; import { renderStringAsDateTime } from "custom/Utils"; import useInterval from "hooks/useInterval"; +import { intl } from "locale/i18n"; import React, { useContext, useEffect, useState } from "react"; import { FormattedMessage } from "react-intl"; import { useParams } from "react-router"; import ApplicationContext from "utils/ApplicationContext"; +import { setFlash } from "utils/Flash"; import { TabView } from "utils/types"; -import { intl } from "../../locale/i18n"; -import { setFlash } from "../../utils/Flash"; - interface IProps { id: string; } @@ -76,8 +82,10 @@ const ServiceTicketDetail = () => { const { id } = useParams(); const [ticket, setTicket] = useState(); const [notFound, setNotFound] = useState(false); - + const [isModalVisible, setIsModalVisible] = useState(false); const { theme, customApiClient, redirect } = useContext(ApplicationContext); + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); useInterval(async () => { if (ticket?.transition_action) { @@ -224,6 +232,25 @@ const ServiceTicketDetail = () => { return ( + {isModalVisible && ( + + + +

Background job logs

+
+
+ + + + + + + + Close + + +
+ )}
@@ -232,11 +259,20 @@ const ServiceTicketDetail = () => {

Service ticket

- + + + + active background job(s) + {ticket?.transition_action === null && ticket?.process_state === "initial" && ( { name={} >
- - {renderLogItemActions(ticket, actions)} - - - + + + + + + + + + + + + + + + + + + + +
diff --git a/src/custom-surf/types.ts b/src/custom-surf/types.ts index b6cf22f03..938465acc 100644 --- a/src/custom-surf/types.ts +++ b/src/custom-surf/types.ts @@ -89,7 +89,6 @@ export interface ServiceTicketImpactedIMSCircuit { ims_circuit_id: number; ims_circuit_name: string; impact: ServiceTicketImpactedObjectImpact; - impact_override?: ServiceTicketImpactedObjectImpact; extra_information?: string; } @@ -114,7 +113,9 @@ export interface ServiceTicketRelatedCustomer { } export interface ServiceTicketImpactedObject { - subscription_id: string; + impact_override: ServiceTicketImpactedObjectImpact; + subscription_id: string | null; + product_type: string; logged_by: string; ims_circuits: ServiceTicketImpactedIMSCircuit[]; owner_customer: ServiceTicketCustomer; @@ -129,6 +130,14 @@ export enum ServiceTicketType { INCIDENT = "incident", } +export interface BackgroundJobLog { + message: string; + customer_id?: string; + subscription_id?: string; + entry_time: string; + process_state: string; +} + export interface ServiceTicketWithDetails extends ServiceTicket { transitioning_state: any; end_date: string; @@ -136,4 +145,23 @@ export interface ServiceTicketWithDetails extends ServiceTicket { type: ServiceTicketType; logs: ServiceTicketLog[]; impacted_objects: ServiceTicketImpactedObject[]; + background_logs: BackgroundJobLog[]; +} + +export interface ImsInfo { + impact: ServiceTicketImpactedObjectImpact; + ims_circuit_id: number; + ims_circuit_name: string; + extra_information?: string; +} + +export interface ImpactedObject { + id: string; + customer: string; + impact: ServiceTicketImpactedObjectImpact; + type: string; + subscription: string; + impact_override?: ServiceTicketImpactedObjectImpact; + subscription_id: string | null; + ims_info: ImsInfo[]; } diff --git a/src/locale/en.ts b/src/locale/en.ts index 4d0381d64..ac74dd4f0 100644 --- a/src/locale/en.ts +++ b/src/locale/en.ts @@ -534,7 +534,10 @@ I18n.translations.en = { impact: "Impact", type: "Type", subscription: "Subscription", - impact_override: "Impact override", + ims_circuit_name: "IMS Name", + ims_circuit_id: "ID", + extra_information: "Extra info", + impact_override: "Override", impact_override_info: "Use this to override the impact, or clear to remove override", }, }, @@ -890,6 +893,9 @@ I18n.translations.en = { backendProblem: "Couldn't query CIM backend", flash: { create_ticket_form: "Create ticket form submitted", + open_ticket_form: "Open ticket form submitted", + update_ticket_form: "Update ticket form submitted", + close_ticket_form: "Close ticket form submitted", }, }, };