diff --git a/web/src/assets/plushie.svg b/web/src/assets/plushie.svg
new file mode 100644
index 0000000000..4c8433e81c
--- /dev/null
+++ b/web/src/assets/plushie.svg
@@ -0,0 +1,9 @@
+
diff --git a/web/src/components/ContactUs/ContactUs.styled.ts b/web/src/components/ContactUs/ContactUs.styled.ts
new file mode 100644
index 0000000000..daddc0d1ff
--- /dev/null
+++ b/web/src/components/ContactUs/ContactUs.styled.ts
@@ -0,0 +1,73 @@
+import styled from 'styled-components';
+import {Modal as AntModal, Button, Typography} from 'antd';
+import Plushie from 'assets/plushie.svg';
+
+export const Container = styled.div`
+ position: absolute;
+ right: 12px;
+ bottom: 12px;
+ cursor: pointer;
+`;
+
+export const PlushieImage = styled.img.attrs({
+ src: Plushie,
+})``;
+
+export const PulseButtonContainer = styled.div`
+ position: absolute;
+ left: 2px;
+ top: -4px;
+`;
+
+export const ModalFooter = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+`;
+
+export const FullWidthButton = styled(Button)`
+ && {
+ && {
+ width: 100%;
+ margin: 0px;
+ }
+ }
+`;
+
+export const Header = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+`;
+
+export const Title = styled(Typography.Title)``;
+
+export const Message = styled(Typography.Paragraph)`
+ && {
+ margin: 0;
+ }
+`;
+
+export const Modal = styled(AntModal).attrs({
+ width: 'auto',
+})`
+ right: 32px;
+ top: calc(100vh - 410px);
+ position: absolute;
+ height: max-content;
+ padding: 0;
+
+ .ant-modal-content {
+ width: 340px;
+ }
+
+ .ant-modal-footer {
+ border: none;
+ padding-top: 0;
+ }
+
+ .ant-modal-body {
+ padding-bottom: 19px;
+ }
+`;
diff --git a/web/src/components/ContactUs/ContactUs.tsx b/web/src/components/ContactUs/ContactUs.tsx
new file mode 100644
index 0000000000..3fb9a86f30
--- /dev/null
+++ b/web/src/components/ContactUs/ContactUs.tsx
@@ -0,0 +1,35 @@
+import {createContext, useContext, useMemo, useState} from 'react';
+import {noop} from 'lodash';
+import * as S from './ContactUs.styled';
+import ContactUsModal from './ContactUsModal';
+import PulseButton from '../PulseButton';
+
+interface IContext {
+ onOpen(): void;
+}
+
+export const Context = createContext({
+ onOpen: noop,
+});
+
+export const useContactUsModal = () => useContext(Context);
+
+const ContactUs: React.FC = ({children}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const value = useMemo(() => ({onOpen: () => setIsOpen(true)}), []);
+
+ return (
+
+ {children}
+ setIsOpen(true)}>
+
+
+
+
+
+ setIsOpen(false)} />
+
+ );
+};
+
+export default ContactUs;
diff --git a/web/src/components/ContactUs/ContactUsModal.tsx b/web/src/components/ContactUs/ContactUsModal.tsx
new file mode 100644
index 0000000000..6f75a01ac1
--- /dev/null
+++ b/web/src/components/ContactUs/ContactUsModal.tsx
@@ -0,0 +1,24 @@
+import * as S from './ContactUs.styled';
+import ContactUsModalFooter from './ContactUsModalFooter';
+
+interface IProps {
+ isOpen: boolean;
+ onClose(): void;
+}
+
+const ContactUsModal = ({isOpen, onClose}: IProps) => {
+ return (
+ }>
+
+
+ Let us help you
+
+
+ Technical glitches can be tricky, even for the best of us. Don't fret! We're here to save your day.
+ Create an Issue or contact us via Discord and our tech-savvy team are ready to lend a helping hand.
+
+
+ );
+};
+
+export default ContactUsModal;
diff --git a/web/src/components/ContactUs/ContactUsModalFooter.tsx b/web/src/components/ContactUs/ContactUsModalFooter.tsx
new file mode 100644
index 0000000000..8fd6221f0a
--- /dev/null
+++ b/web/src/components/ContactUs/ContactUsModalFooter.tsx
@@ -0,0 +1,19 @@
+import {DISCORD_URL, GITHUB_ISSUES_URL} from 'constants/Common.constants';
+import * as S from './ContactUs.styled';
+
+const ContactUsModalFooter = () => {
+ return (
+
+
+ Create an Issue
+
+
+
+ Contact Team on Discord
+
+
+
+ );
+};
+
+export default ContactUsModalFooter;
diff --git a/web/src/components/ContactUs/index.ts b/web/src/components/ContactUs/index.ts
new file mode 100644
index 0000000000..5b52ead37b
--- /dev/null
+++ b/web/src/components/ContactUs/index.ts
@@ -0,0 +1,5 @@
+import {useContactUsModal} from './ContactUs';
+
+// eslint-disable-next-line no-restricted-exports
+export {default} from './ContactUs';
+export {useContactUsModal};
diff --git a/web/src/components/PulseButton/PulseButton.styled.ts b/web/src/components/PulseButton/PulseButton.styled.ts
new file mode 100644
index 0000000000..98d9294e2e
--- /dev/null
+++ b/web/src/components/PulseButton/PulseButton.styled.ts
@@ -0,0 +1,31 @@
+import styled, {DefaultTheme, keyframes} from 'styled-components';
+
+const getPulseAnimation = (theme: DefaultTheme) =>
+ keyframes`
+ 0% {
+ transform: scale(.7);
+ box-shadow: 0 0 0 0 ${theme.color.primaryLight};
+ }
+ 70% {
+ transform: scale(1);
+ box-shadow: 0 0 0 2px ${theme.color.primaryLight};
+ }
+ 100% {
+ transform: scale(.7);
+ box-shadow: 0 0 0 0 ${theme.color.primaryLight};
+ }
+`;
+
+export const PulseButton = styled.button`
+ width: 9px;
+ height: 9px;
+ border: none;
+ padding: 0px;
+ border-radius: 50%;
+ cursor: pointer;
+ background: ${({theme}) => theme.color.primary};
+
+ animation-name: ${({theme}) => getPulseAnimation(theme)};
+ animation-duration: 1.5s;
+ animation-iteration-count: infinite;
+`;
diff --git a/web/src/components/PulseButton/index.ts b/web/src/components/PulseButton/index.ts
new file mode 100644
index 0000000000..33f5035b60
--- /dev/null
+++ b/web/src/components/PulseButton/index.ts
@@ -0,0 +1,3 @@
+import {PulseButton} from './PulseButton.styled';
+
+export default PulseButton;
diff --git a/web/src/pages/Settings/Settings.tsx b/web/src/pages/Settings/Settings.tsx
index 10c0bfd153..c382f551ed 100644
--- a/web/src/pages/Settings/Settings.tsx
+++ b/web/src/pages/Settings/Settings.tsx
@@ -3,14 +3,17 @@ import withAnalytics from 'components/WithAnalytics/WithAnalytics';
import DataStoreProvider from 'providers/DataStore';
import SettingsProvider from 'providers/Settings';
import Content from './Content';
+import ContactUs from '../../components/ContactUs/ContactUs';
const Settings = () => (
-
-
-
-
-
+
+
+
+
+
+
+
);
diff --git a/web/src/providers/DataStore/DataStore.provider.tsx b/web/src/providers/DataStore/DataStore.provider.tsx
index 34f9938bee..b67ec01f0d 100644
--- a/web/src/providers/DataStore/DataStore.provider.tsx
+++ b/web/src/providers/DataStore/DataStore.provider.tsx
@@ -9,6 +9,7 @@ import {
useDeleteDataStoreMutation,
} from 'redux/apis/TraceTest.api';
import DataStoreService from 'services/DataStore.service';
+import {useContactUsModal} from 'components/ContactUs';
import {SupportedDataStores, TConnectionResult, TDraftDataStore} from 'types/DataStore.types';
import DataStore from 'models/DataStore.model';
import useDataStoreNotification from './hooks/useDataStoreNotification';
@@ -49,6 +50,8 @@ const DataStoreProvider = ({children}: IProps) => {
const [isFormValid, setIsFormValid] = useState(false);
const {showSuccessNotification, showTestConnectionNotification} = useDataStoreNotification();
const {onOpen} = useConfirmationModal();
+ const [connectionTries, setConnectionTries] = useState(0);
+ const {onOpen: onContactUsOpen} = useContactUsModal();
const onSaveConfig = useCallback(
async (draft: TDraftDataStore, defaultDataStore: DataStore) => {
@@ -105,11 +108,16 @@ const DataStoreProvider = ({children}: IProps) => {
try {
const result = await testConnection(dataStore.spec!).unwrap();
showTestConnectionNotification(result, draft.dataStoreType!);
+ setConnectionTries(0);
} catch (err) {
+ setConnectionTries(prev => prev + 1);
showTestConnectionNotification(err as TConnectionResult, draft.dataStoreType!);
+ if (connectionTries + 1 === 3) {
+ onContactUsOpen();
+ }
}
},
- [showTestConnectionNotification, testConnection]
+ [connectionTries, onContactUsOpen, showTestConnectionNotification, testConnection]
);
const value = useMemo(