diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index d9f10327a9..25680f676e 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -63,7 +63,7 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT HandlerFunc(middleware.EnforceAccess(policy.AppSupportbundleWrite, handler.CollectHelmSupportBundle)) r.Name("ShareSupportBundle").Path("/api/v1/troubleshoot/app/{appSlug}/supportbundle/{bundleId}/share").Methods("POST"). HandlerFunc(middleware.EnforceAccess(policy.AppSupportbundleWrite, handler.ShareSupportBundle)) - r.Name("DeleteSupportBundle").Path("/api/v1/troubleshoot/app/{appSlug}/supportbundle/{bundleId}/delete").Methods("DELETE"). + r.Name("DeleteSupportBundle").Path("/api/v1/troubleshoot/app/{appSlug}/supportbundle/{bundleId}").Methods("DELETE"). HandlerFunc(middleware.EnforceAccess(policy.AppSupportbundleWrite, handler.DeleteSupportBundle)) r.Name("GetPodDetailsFromSupportBundle").Path("/api/v1/troubleshoot/app/{appSlug}/supportbundle/{bundleId}/pod").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.AppSupportbundleRead, handler.GetPodDetailsFromSupportBundle)) diff --git a/web/src/Root.tsx b/web/src/Root.tsx index e8b420f6c9..83a9c15cb5 100644 --- a/web/src/Root.tsx +++ b/web/src/Root.tsx @@ -36,6 +36,7 @@ import connectHistory from "./services/matomo"; // types import { App, Metadata, ThemeState } from "@types"; +import { ToastProvider } from "./context/ToastContext"; // react-query client const queryClient = new QueryClient(); @@ -448,208 +449,212 @@ const Root = () => { clearThemeState, }} > - - -
- - ( - - )} - /> - { - const Crashz = () => { - throw new Error("Crashz!"); - }; - return ; - }} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - } - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - - -
-
-
-
+ + + +
+ + ( + + )} + /> + { + const Crashz = () => { + throw new Error("Crashz!"); + }; + return ; + }} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + } + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + +
+
+
+
+
{ + switch (type) { + case "success": + return "tw-bg-[#38cc97]"; + case "error": + return "tw-bg-[#f65c5c]"; + case "warning": + return "tw-bg-[#FFA500]"; + default: + return "tw-bg-[#38cc97]"; + } +}; + +const Toast = ({ isToastVisible, type, children }: ToastProps) => { + return ( +
+
+
+ {children} +
+
+ ); +}; + +export default Toast; diff --git a/web/src/components/troubleshoot/SupportBundleList.tsx b/web/src/components/troubleshoot/SupportBundleList.tsx index dd5c14d53d..9d5577130c 100644 --- a/web/src/components/troubleshoot/SupportBundleList.tsx +++ b/web/src/components/troubleshoot/SupportBundleList.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useReducer, useEffect, useContext } from "react"; import { KotsPageTitle } from "@components/Head"; import { withRouter, @@ -13,12 +14,13 @@ import ConfigureRedactorsModal from "./ConfigureRedactorsModal"; import ErrorModal from "../modals/ErrorModal"; import { Utilities } from "../../utilities/utilities"; import { Repeater } from "@src/utilities/repeater"; - import "../../scss/components/troubleshoot/SupportBundleList.scss"; import Icon from "../Icon"; - import { App, SupportBundle, SupportBundleProgress } from "@types"; import GenerateSupportBundleModal from "./GenerateSupportBundleModal"; +import { useHistory } from "react-router-dom"; +import { ToastContext } from "@src/context/ToastContext"; +import Toast from "@components/shared/Toast"; type Props = { bundle: SupportBundle; @@ -52,58 +54,42 @@ type State = { isGeneratingBundleOpen: boolean; }; -class SupportBundleList extends React.Component { - constructor(props: Props) { - super(props); - this.state = { +export const SupportBundleList = (props: Props) => { + const [state, setState] = useReducer( + (currentState: State, newState: Partial) => ({ + ...currentState, + ...newState, + }), + { displayRedactorModal: false, loadingSupportBundles: false, pollForBundleAnalysisProgress: new Repeater(), isGeneratingBundleOpen: false, - }; - } - - componentDidMount() { - this.listSupportBundles(); - } - - componentWillUnmount() { - this.state.pollForBundleAnalysisProgress.stop(); - } - - componentDidUpdate(lastProps: Props) { - const { bundle } = this.props; - if ( - bundle?.status !== "running" && - bundle?.status !== lastProps.bundle.status - ) { - this.listSupportBundles(); - this.state.pollForBundleAnalysisProgress.stop(); - if (bundle.status === "failed") { - this.props.history.push(`/app/${this.props.watch?.slug}/troubleshoot`); - } } - } + ); - toggleGenerateBundleModal = () => { - this.setState({ - isGeneratingBundleOpen: !this.state.isGeneratingBundleOpen, - }); - }; + const history = useHistory(); + const { + deleteBundleId, + setIsToastVisible, + isToastVisible, + toastMessage, + setIsCancelled, + } = useContext(ToastContext); - listSupportBundles = () => { - this.setState({ + const listSupportBundles = () => { + setState({ errorMsg: "", }); - this.props.updateState({ + props.updateState({ loading: true, displayErrorModal: true, loadingBundle: false, }); fetch( - `${process.env.API_ENDPOINT}/troubleshoot/app/${this.props.watch?.slug}/supportbundles`, + `${process.env.API_ENDPOINT}/troubleshoot/app/${props.watch?.slug}/supportbundles`, { headers: { Authorization: Utilities.getToken(), @@ -114,10 +100,10 @@ class SupportBundleList extends React.Component { ) .then(async (res) => { if (!res.ok) { - this.setState({ + setState({ errorMsg: `Unexpected status code: ${res.status}`, }); - this.props.updateState({ loading: false, displayErrorModal: true }); + props.updateState({ loading: false, displayErrorModal: true }); return; } const response = await res.json(); @@ -129,97 +115,118 @@ class SupportBundleList extends React.Component { ); } if (bundleRunning) { - this.state.pollForBundleAnalysisProgress.start( - this.props.pollForBundleAnalysisProgress, + state.pollForBundleAnalysisProgress.start( + props.pollForBundleAnalysisProgress, 1000 ); } - this.setState({ + setState({ supportBundles: response.supportBundles, errorMsg: "", }); - this.props.updateState({ loading: false, displayErrorModal: false }); + props.updateState({ loading: false, displayErrorModal: false }); }) .catch((err) => { console.log(err); - this.setState({ + setState({ errorMsg: err ? err.message : "Something went wrong, please try again.", }); - this.props.updateState({ displayErrorModal: true, loading: false }); + props.updateState({ displayErrorModal: true, loading: false }); }); }; - toggleErrorModal = () => { - this.props.updateState({ - displayErrorModal: !this.props.displayErrorModal, + useEffect(() => { + listSupportBundles(); + return () => { + state.pollForBundleAnalysisProgress.stop(); + }; + }, []); + + useEffect(() => { + const { bundle } = props; + if (bundle?.status !== "running") { + listSupportBundles(); + state.pollForBundleAnalysisProgress.stop(); + if (bundle.status === "failed") { + history.push(`/app/${props.watch?.slug}/troubleshoot`); + } + } + }, [props.bundle]); + + const toggleGenerateBundleModal = () => { + setState({ + isGeneratingBundleOpen: !state.isGeneratingBundleOpen, }); }; - toggleRedactorModal = () => { - this.setState({ - displayRedactorModal: !this.state.displayRedactorModal, + const toggleErrorModal = () => { + props.updateState({ + displayErrorModal: !props.displayErrorModal, }); }; - render() { - const { watch, loading, loadingBundle } = this.props; - const { errorMsg, supportBundles, isGeneratingBundleOpen } = this.state; + const toggleRedactorModal = () => { + setState({ + displayRedactorModal: !state.displayRedactorModal, + }); + }; - const downstream = watch?.downstream; + const { watch, loading, loadingBundle } = props; + const { errorMsg, supportBundles, isGeneratingBundleOpen } = state; - if (loading) { - return ( -
- -
- ); - } + const downstream = watch?.downstream; - let bundlesNode; - if (downstream) { - if (supportBundles?.length) { - bundlesNode = supportBundles - .sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ) - .map((bundle) => ( - - )); - } else { - return ( - + + + ); + } + + let bundlesNode; + if (downstream) { + if (supportBundles?.length) { + bundlesNode = supportBundles + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) + .map((bundle) => ( + - ); - } + )); + } else { + return ( + + ); } + } - return ( + return ( + <>
@@ -229,16 +236,14 @@ class SupportBundleList extends React.Component { { title: "Support bundles", onClick: () => - this.props.history.push( - `/app/${this.props.watch?.slug}/troubleshoot` - ), + history.push(`/app/${props.watch?.slug}/troubleshoot`), isActive: true, }, { title: "Redactors", onClick: () => - this.props.history.push( - `/app/${this.props.watch?.slug}/troubleshoot/redactors` + history.push( + `/app/${props.watch?.slug}/troubleshoot/redactors` ), isActive: false, }, @@ -257,7 +262,7 @@ class SupportBundleList extends React.Component {
- {this.state.displayRedactorModal && ( - + {state.displayRedactorModal && ( + )} {errorMsg && ( )} - ); - } -} + + +
+

{toastMessage}

+ setIsCancelled(true)} + className="tw-underline tw-cursor-pointer" + > + undo + + setIsToastVisible(false)} + /> +
+
+ + ); +}; /* eslint-disable */ // @ts-ignore diff --git a/web/src/components/troubleshoot/SupportBundleRow.tsx b/web/src/components/troubleshoot/SupportBundleRow.tsx index 9f8b6db11d..327072ab1d 100644 --- a/web/src/components/troubleshoot/SupportBundleRow.tsx +++ b/web/src/components/troubleshoot/SupportBundleRow.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import React, { useEffect, useContext } from "react"; import { withRouter, withRouterType, @@ -17,6 +17,8 @@ import { SupportBundleInsight, SupportBundleProgress, } from "@types"; +import { useHistory } from "react-router"; +import { ToastContext } from "@src/context/ToastContext"; let percentage: number; @@ -29,6 +31,7 @@ type Props = { progressData: SupportBundleProgress; refetchBundleList: () => void; watchSlug: string; + className: string; } & withRouterType; type State = { @@ -39,40 +42,77 @@ type State = { sendingBundle: boolean; sendingBundleErrMsg?: string; warningInsights?: SupportBundleInsight[]; + timeoutId?: ReturnType; }; -class SupportBundleRow extends React.Component { - constructor(props: Props) { - super(props); +export const SupportBundleRow = (props: Props) => { + const history = useHistory(); + const { + setIsToastVisible, + isCancelled, + setIsCancelled, + setDeleteBundleId, + setToastMessage, + } = useContext(ToastContext); - this.state = { + const [state, setState] = React.useReducer( + (currentState: State, newState: Partial) => ({ + ...currentState, + ...newState, + }), + { downloadingBundle: false, sendingBundle: false, - }; - } + } + ); - renderSharedContext = () => { - const { bundle } = this.props; + const renderSharedContext = () => { + const { bundle } = props; if (!bundle) { return null; } }; - componentDidMount() { - if (this.props.bundle) { - this.buildInsights(); + const buildInsights = () => { + const { bundle } = props; + if (!bundle?.analysis?.insights) { + return; } - } + const errorInsights = filter(bundle.analysis.insights, [ + "severity", + "error", + ]); + const warningInsights = filter(bundle.analysis.insights, [ + "severity", + "warn", + ]); + const otherInsights = filter(bundle.analysis.insights, (item) => { + return ( + item.severity === null || + item.severity === "info" || + item.severity === "debug" + ); + }); + setState({ + errorInsights, + warningInsights, + otherInsights, + }); + }; - handleBundleClick = (bundle: SupportBundle) => { - const { watchSlug } = this.props; - this.props.history.push( - `/app/${watchSlug}/troubleshoot/analyze/${bundle.slug}` - ); + useEffect(() => { + if (props.bundle) { + buildInsights(); + } + }, []); + + const handleBundleClick = (bundle: SupportBundle) => { + const { watchSlug } = props; + history.push(`/app/${watchSlug}/troubleshoot/analyze/${bundle.slug}`); }; - downloadBundle = async (bundle: SupportBundle) => { - this.setState({ downloadingBundle: true, downloadBundleErrMsg: "" }); + const downloadBundle = async (bundle: SupportBundle) => { + setState({ downloadingBundle: true, downloadBundleErrMsg: "" }); fetch( `${process.env.API_ENDPOINT}/troubleshoot/supportbundle/${bundle.id}/download`, { @@ -84,7 +124,7 @@ class SupportBundleRow extends React.Component { ) .then(async (result) => { if (!result.ok) { - this.setState({ + setState({ downloadingBundle: false, downloadBundleErrMsg: `Unable to download bundle: Status ${result.status}, please try again.`, }); @@ -105,11 +145,11 @@ class SupportBundleRow extends React.Component { const blob = await result.blob(); download(blob, filename, "application/gzip"); - this.setState({ downloadingBundle: false, downloadBundleErrMsg: "" }); + setState({ downloadingBundle: false, downloadBundleErrMsg: "" }); }) .catch((err) => { console.log(err); - this.setState({ + setState({ downloadingBundle: false, downloadBundleErrMsg: err ? `Unable to download bundle: ${err.message}` @@ -118,14 +158,61 @@ class SupportBundleRow extends React.Component { }); }; - sendBundleToVendor = async (bundleSlug: string) => { - this.setState({ + useEffect(() => { + if (isCancelled && state.timeoutId) { + clearTimeout(state.timeoutId); + setIsToastVisible(false); + setDeleteBundleId(""); + setIsCancelled(false); + } + }, [isCancelled]); + + const deleteBundle = (bundle: SupportBundle) => { + const { match } = props; + const delayFetch = 5000; + const bundleCollectionDate = dayjs(bundle?.createdAt)?.format( + "MMMM D, YYYY @ h:mm a" + ); + setToastMessage(`Deleting bundle collected on ${bundleCollectionDate}.`); + setIsToastVisible(true); + setDeleteBundleId(bundle.id); + + let id = setTimeout(() => { + fetch( + `${process.env.API_ENDPOINT}/troubleshoot/app/${match.params.slug}/supportbundle/${bundle.id}`, + { + method: "DELETE", + headers: { + Authorization: Utilities.getToken(), + }, + } + ) + .then(async () => { + setIsToastVisible(false); + + props.refetchBundleList(); + setDeleteBundleId(""); + clearInterval(id); + }) + .catch((err) => { + console.log(err); + setToastMessage("Unable to delete bundle, please try again."); + setDeleteBundleId(""); + clearInterval(id); + }); + }, delayFetch); + + setState({ timeoutId: id }); + }; + + const sendBundleToVendor = async (bundleSlug: string) => { + setState({ sendingBundle: true, sendingBundleErrMsg: "", downloadBundleErrMsg: "", }); fetch( - `${process.env.API_ENDPOINT}/troubleshoot/app/${this.props.match.params.slug}/supportbundle/${bundleSlug}/share`, + `${process.env.API_ENDPOINT}/troubleshoot/app/${props.match.params.slug}/supportbundle/${bundleSlug}/share`, { method: "POST", headers: { @@ -135,18 +222,18 @@ class SupportBundleRow extends React.Component { ) .then(async (result) => { if (!result.ok) { - this.setState({ + setState({ sendingBundle: false, sendingBundleErrMsg: `Unable to send bundle to vendor: Status ${result.status}, please try again.`, }); return; } - await this.props.refetchBundleList(); - this.setState({ sendingBundle: false, sendingBundleErrMsg: "" }); + await props.refetchBundleList(); + setState({ sendingBundle: false, sendingBundleErrMsg: "" }); }) .catch((err) => { console.log(err); - this.setState({ + setState({ sendingBundle: false, sendingBundleErrMsg: err ? `Unable to send bundle to vendor: ${err.message}` @@ -155,34 +242,7 @@ class SupportBundleRow extends React.Component { }); }; - buildInsights = () => { - const { bundle } = this.props; - if (!bundle?.analysis?.insights) { - return; - } - const errorInsights = filter(bundle.analysis.insights, [ - "severity", - "error", - ]); - const warningInsights = filter(bundle.analysis.insights, [ - "severity", - "warn", - ]); - const otherInsights = filter(bundle.analysis.insights, (item) => { - return ( - item.severity === null || - item.severity === "info" || - item.severity === "debug" - ); - }); - this.setState({ - errorInsights, - warningInsights, - otherInsights, - }); - }; - - moveBar(progressData: SupportBundleProgress) { + const moveBar = (progressData: SupportBundleProgress) => { const elem = document.getElementById("supportBundleStatusBar"); const calcPercent = Math.round( (progressData.collectorsCompleted / progressData.collectorCount) * 100 @@ -191,236 +251,237 @@ class SupportBundleRow extends React.Component { if (elem) { elem.style.width = percentage.toString() + "%"; } - } + }; - render() { - const { - bundle, - isSupportBundleUploadSupported, - isAirgap, - progressData, - loadingBundle, - } = this.props; - const { errorInsights, warningInsights, otherInsights } = this.state; + const { + bundle, + isSupportBundleUploadSupported, + isAirgap, + progressData, + loadingBundle, + } = props; + const { errorInsights, warningInsights, otherInsights } = state; - const showSendSupportBundleLink = - isSupportBundleUploadSupported && !isAirgap; + const showSendSupportBundleLink = isSupportBundleUploadSupported && !isAirgap; - if (!bundle) { - return null; - } + if (!bundle) { + return null; + } - let noInsightsMessage; - if (bundle && isEmpty(bundle?.analysis?.insights?.length)) { - if (bundle.status === "uploaded" || bundle.status === "analyzing") { - noInsightsMessage = ( -
- -

- We are still analyzing your bundle -

-
- ); - } else { - noInsightsMessage = ( -

- Unable to surface insights for this bundle + let noInsightsMessage; + if (bundle && isEmpty(bundle?.analysis?.insights?.length)) { + if (bundle.status === "uploaded" || bundle.status === "analyzing") { + noInsightsMessage = ( +

+ +

+ We are still analyzing your bundle

- ); - } - } - - let progressBar; - - let statusDiv = ( -
-
- {progressData?.message && ( - - )} - {percentage >= 98 ? ( -

Almost done, finalizing your bundle...

- ) : ( -

Analyzing {progressData?.message}

- )} -
-
- ); - - if (progressData.collectorsCompleted > 0) { - this.moveBar(progressData); - progressBar = ( -
-
); } else { - percentage = 0; - progressBar = ( -
-
-
+ noInsightsMessage = ( +

+ Unable to surface insights for this bundle +

); } + } - return ( -
-
-
-
-
this.handleBundleClick(bundle)} - > -
- {!this.props.isCustomer ? ( -
- - - Collected on{" "} - - {dayjs(bundle.createdAt).format( - "MMMM D, YYYY @ h:mm a" - )} - - - -
- ) : ( -
+ let progressBar; + + let statusDiv = ( +
+
+ {progressData?.message && ( + + )} + {percentage >= 98 ? ( +

Almost done, finalizing your bundle...

+ ) : ( +

Analyzing {progressData?.message}

+ )} +
+
+ ); + + if (progressData.collectorsCompleted > 0) { + moveBar(progressData); + progressBar = ( +
+
+
+ ); + } else { + percentage = 0; + progressBar = ( +
+
+
+ ); + } + + return ( +
+
+
+
+
handleBundleClick(bundle)} + > +
+ {!props.isCustomer ? ( +
+ - - Collected on{" "} - - {dayjs(bundle.createdAt).format( - "MMMM D, YYYY @ h:mm a" - )} - + Collected on{" "} + + {dayjs(bundle.createdAt).format( + "MMMM D, YYYY @ h:mm a" + )} - {this.renderSharedContext()} -
- )} -
-
- {this.props.loadingBundle ? ( - statusDiv - ) : bundle?.analysis?.insights?.length ? ( -
- {errorInsights && errorInsights.length > 0 && ( - - - {errorInsights.length} error - {errorInsights.length > 1 ? "s" : ""} found - - )} - {warningInsights && warningInsights.length > 0 && ( - - - {warningInsights.length} warning - {warningInsights.length > 1 ? "s" : ""} found - - )} - {otherInsights && otherInsights.length > 0 && ( - - - {otherInsights.length} informational and debugging - insight{otherInsights.length > 1 ? "s" : ""} found + +
+ ) : ( +
+ + + Collected on{" "} + + {dayjs(bundle.createdAt).format( + "MMMM D, YYYY @ h:mm a" + )} - )} -
- ) : ( - noInsightsMessage - )} -
-
-
- {this.state.sendingBundleErrMsg && ( -

- {this.state.sendingBundleErrMsg} -

- )} - {this.props.bundle.sharedAt ? ( -
- - - Sent to vendor on{" "} - {Utilities.dateFormat(bundle.sharedAt, "MM/DD/YYYY")} + + {renderSharedContext()}
- ) : this.state.sendingBundle ? ( - - ) : showSendSupportBundleLink && !loadingBundle ? ( - - this.sendBundleToVendor(this.props.bundle.slug) - } - > - - - ) : null} - {this.state.downloadBundleErrMsg && ( -

- {this.state.downloadBundleErrMsg} -

)} - {this.state.downloadingBundle ? ( - - ) : this.props.loadingBundle || - this.props.progressData?.collectorsCompleted > 0 ? ( -
- - {percentage.toString() + "%"} - - {progressBar} - - 100% - +
+
+ {props.loadingBundle ? ( + statusDiv + ) : bundle?.analysis?.insights?.length ? ( +
+ {errorInsights && errorInsights.length > 0 && ( + + + {errorInsights.length} error + {errorInsights.length > 1 ? "s" : ""} found + + )} + {warningInsights && warningInsights.length > 0 && ( + + + {warningInsights.length} warning + {warningInsights.length > 1 ? "s" : ""} found + + )} + {otherInsights && otherInsights.length > 0 && ( + + + {otherInsights.length} informational and debugging + insight{otherInsights.length > 1 ? "s" : ""} found + + )}
) : ( - this.downloadBundle(bundle)} - > - - + noInsightsMessage )}
+
+ {state.sendingBundleErrMsg && ( +

+ {state.sendingBundleErrMsg} +

+ )} + {props.bundle.sharedAt ? ( +
+ + + Sent to vendor on{" "} + {Utilities.dateFormat(bundle.sharedAt, "MM/DD/YYYY")} + +
+ ) : state.sendingBundle ? ( + + ) : showSendSupportBundleLink && !loadingBundle ? ( + sendBundleToVendor(props.bundle.slug)} + > + + + ) : null} + {state.downloadBundleErrMsg && ( +

+ {state.downloadBundleErrMsg} +

+ )} + {state.downloadingBundle ? ( + + ) : props.loadingBundle || + props.progressData?.collectorsCompleted > 0 ? ( +
+ + {percentage.toString() + "%"} + + {progressBar} + + 100% + +
+ ) : ( + downloadBundle(bundle)} + > + + + )} + deleteBundle(bundle)} + > + + +
- ); - } -} +
+ ); +}; /* eslint-disable */ // @ts-ignore diff --git a/web/src/context/ToastContext.tsx b/web/src/context/ToastContext.tsx new file mode 100644 index 0000000000..ca07a6293f --- /dev/null +++ b/web/src/context/ToastContext.tsx @@ -0,0 +1,39 @@ +import React, { createContext, ReactNode, useState } from "react"; + +interface ToastContextProps { + isToastVisible: boolean; + setIsToastVisible: (val: boolean) => void; + isCancelled: boolean; + setIsCancelled: (val: boolean) => void; + deleteBundleId: string; + setDeleteBundleId: (val: string) => void; + toastMessage: string; + setToastMessage: (val: string) => void; +} + +const ToastContext = createContext({} as ToastContextProps); + +const ToastProvider = ({ children }: { children: ReactNode }) => { + const [isToastVisible, setIsToastVisible] = useState(false); + const [isCancelled, setIsCancelled] = useState(false); + const [deleteBundleId, setDeleteBundleId] = useState(""); + const [toastMessage, setToastMessage] = useState(""); + return ( + + {children} + + ); +}; + +export { ToastContext, ToastProvider }; diff --git a/web/src/scss/components/troubleshoot/SupportBundleList.scss b/web/src/scss/components/troubleshoot/SupportBundleList.scss index a20216bede..d726a5374d 100644 --- a/web/src/scss/components/troubleshoot/SupportBundleList.scss +++ b/web/src/scss/components/troubleshoot/SupportBundleList.scss @@ -86,6 +86,16 @@ } } } +.deleting { + animation: blinker 3s linear infinite; + cursor: not-allowed !important; +} + +@keyframes blinker { + 50% { + opacity: 0.2; + } +} .CommunityLicenseBundle--wrapper { max-height: 94px; diff --git a/web/src/scss/utilities/base.scss b/web/src/scss/utilities/base.scss index ead31a26da..fddef1f017 100644 --- a/web/src/scss/utilities/base.scss +++ b/web/src/scss/utilities/base.scss @@ -512,3 +512,49 @@ and `background-position: [value]` color: $text-color-primary; line-height: 1.7; } + +@mixin keyframes($animation-name) { + @-webkit-keyframes #{$animation-name} { + @content; + } + @keyframes #{$animation-name} { + @content; + } +} + +@mixin animation($str) { + -webkit-animation: #{$str}; + animation: #{$str}; +} + +@include keyframes(fadein) { + from { + bottom: 0; + opacity: 0; + } + to { + bottom: 10px; + opacity: 1; + } +} + +@include keyframes(fadeout) { + from { + bottom: 10px; + opacity: 1; + } + to { + bottom: 0; + opacity: 0; + } +} + +.toast { + visibility: hidden; + bottom: 3px; +} + +.toast.visible { + visibility: visible; + @include animation("fadein 0.5s, fadeout 0.5s 5s"); +} diff --git a/web/src/stories/Toast.stories.tsx b/web/src/stories/Toast.stories.tsx new file mode 100644 index 0000000000..b8b7fca355 --- /dev/null +++ b/web/src/stories/Toast.stories.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { ComponentStory, ComponentMeta } from "@storybook/react"; +import Toast from "@src/components/shared/Toast"; +import Icon from "@src/components/Icon"; + +export default { + title: "Example/Toast", + component: Toast, +} as ComponentMeta; + +const Template: ComponentStory = () => ( +
+
+ +
+

Success!

+ alert("close toast")} + /> +
+
+
+
+ +
+

Deleting item

+ alert("undo")} + className="tw-underline tw-cursor-pointer" + > + undo + + alert("close toast")} + /> +
+
+
+
+ +
+

Error! Please do something!

+ alert("close toast")} + /> +
+
+
+
+); + +export const ToastExample = Template.bind({});