react-icons/fi#FiTrash TypeScript Examples

The following examples show how to use react-icons/fi#FiTrash. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: WebhooksTable.tsx    From bluebubbles-server with Apache License 2.0 5 votes vote down vote up
WebhooksTable = ({ webhooks }: { webhooks: Array<WebhookItem> }): JSX.Element => {
    const dispatch = useAppDispatch();
    const dialogRef = useRef(null);
    const [selectedId, setSelectedId] = useState(undefined as number | undefined);
    return (
        <Box>
            <Table variant="striped" colorScheme="blue">
                <TableCaption>These are callbacks to receive events from the BlueBubbles Server</TableCaption>
                <Thead>
                    <Tr>
                        <Th>URL</Th>
                        <Th>Event Subscriptions</Th>
                        <Th isNumeric>Actions</Th>
                    </Tr>
                </Thead>
                <Tbody>
                    {webhooks.map(item => (
                        <Tr key={item.id}>
                            <Td>{item.url}</Td>
                            <Td>{JSON.parse(item.events).map((e: string) => webhookEventValueToLabel(e)).join(', ')}</Td>
                            <Td isNumeric>
                                <Grid templateColumns="repeat(2, 1fr)">
                                    <Tooltip label='Edit' placement='bottom'>
                                        <GridItem _hover={{ cursor: 'pointer' }} onClick={() => setSelectedId(item.id)}>
                                            <Icon as={AiOutlineEdit} />
                                        </GridItem>
                                    </Tooltip>
                                    
                                    <Tooltip label='Delete' placement='bottom'>
                                        <GridItem _hover={{ cursor: 'pointer' }} onClick={() => dispatch(remove(item.id))}>
                                            <Icon as={FiTrash} />
                                        </GridItem>
                                    </Tooltip>
                                </Grid>
                            </Td>
                        </Tr>
                    ))}
                </Tbody>
            </Table>

            <AddWebhookDialog
                existingId={selectedId}
                modalRef={dialogRef}
                isOpen={!!selectedId}
                onClose={() => {
                    setSelectedId(undefined);
                }}
            />
        </Box>
    );
}
Example #2
Source File: Navigation.tsx    From bluebubbles-server with Apache License 2.0 5 votes vote down vote up
Navigation = (): JSX.Element => {
    const { isOpen, onOpen, onClose } = useDisclosure();
    const {
        isOpen: isNotificationsOpen,
        onOpen: onNotificationOpen,
        onClose: onNotificationClose
    } = useDisclosure();

    const notifications: Array<NotificationItem> = useAppSelector(state => state.notificationStore.notifications);
    const unreadCount = notifications.filter(e => !e.read).length;
    const dispatch = useAppDispatch();

    return (
        <Box minH="100vh">
            <Router>
                <SidebarContent onClose={() => onClose} display={{ base: 'none', md: 'block' }} />
                <Drawer
                    autoFocus={false}
                    isOpen={isOpen}
                    placement="left"
                    onClose={onClose}
                    returnFocusOnClose={false}
                    onOverlayClick={onClose}
                    size="full"
                >
                    <DrawerContent>
                        <SidebarContent onClose={onClose} />
                    </DrawerContent>
                </Drawer>
                {/* mobilenav */}
                <MobileNav onOpen={onOpen} onNotificationOpen={onNotificationOpen} unreadCount={unreadCount} />
                <Box ml={{ base: 0, md: 60 }} p="2">
                    <Routes>
                        <Route path="/settings" element={<SettingsLayout />} />
                        <Route path="/logs" element={<LogsLayout />} />
                        <Route path="/contacts" element={<ContactsLayout />} />
                        <Route path="/fcm" element={<FcmLayout />} />
                        <Route path="/devices" element={<DevicesLayout />} />
                        <Route path="/webhooks" element={<ApiLayout />} />
                        <Route path="/guides" element={<GuidesLayout />} />
                        <Route path="/" element={<HomeLayout />} />
                    </Routes>
                </Box>
            </Router>

            <Drawer onClose={() => closeNotification(onNotificationClose, dispatch)} isOpen={isNotificationsOpen} size="lg">
                <DrawerOverlay />
                <DrawerContent>
                    <DrawerHeader>Notifications / Alerts ({unreadCount})</DrawerHeader>
                    <DrawerBody>
                        <Menu>
                            <MenuButton
                                as={Button}
                                rightIcon={<BsChevronDown />}
                                width="12em"mr={5}
                                mb={4}
                            >
                                Manage
                            </MenuButton>
                            <MenuList>
                                <MenuItem icon={<FiTrash />} onClick={() => {
                                    dispatch(clearAlerts());
                                }}>
                                    Clear Alerts
                                </MenuItem>
                                <MenuItem icon={<BsCheckAll />} onClick={() => {
                                    dispatch(readAll());
                                }}>
                                    Mark All as Read
                                </MenuItem>
                            </MenuList>
                        </Menu>
                        <NotificationsTable notifications={notifications} />
                    </DrawerBody>
                </DrawerContent>
            </Drawer>
        </Box>
    );
}
Example #3
Source File: index.tsx    From rocketseat-gostack-11-desafios with MIT License 5 votes vote down vote up
Food: React.FC<IProps> = ({
  food,
  handleDelete,
  handleEditFood,
  toggleModal,
}: IProps) => {
  const [isAvailable, setIsAvailable] = useState(food.available);

  async function toggleAvailable(): Promise<void> {
    await api.put(`/foods/${food.id}`, { ...food, available: !isAvailable });

    setIsAvailable(state => !state);
  }

  function setEditingFood(): void {
    handleEditFood(food);
    toggleModal();
  }

  return (
    <Container available={isAvailable}>
      <header>
        <img src={food.image} alt={food.name} />
      </header>
      <section className="body">
        <h2>{food.name}</h2>
        <p>{food.description}</p>
        <p className="price">
          R$ <b>{food.price}</b>
        </p>
      </section>
      <section className="footer">
        <div className="icon-container">
          <button
            type="button"
            className="icon"
            onClick={() => setEditingFood()}
            data-testid={`edit-food-${food.id}`}
          >
            <FiEdit3 size={20} />
          </button>

          <button
            type="button"
            className="icon"
            onClick={() => handleDelete(food.id)}
            data-testid={`remove-food-${food.id}`}
          >
            <FiTrash size={20} />
          </button>
        </div>

        <div className="availability-container">
          <p>{isAvailable ? 'Disponível' : 'Indisponível'}</p>

          <label htmlFor={`available-switch-${food.id}`} className="switch">
            <input
              id={`available-switch-${food.id}`}
              type="checkbox"
              checked={isAvailable}
              onChange={toggleAvailable}
              data-testid={`change-status-food-${food.id}`}
            />
            <span className="slider" />
          </label>
        </div>
      </section>
    </Container>
  );
}
Example #4
Source File: index.tsx    From ecoleta with MIT License 5 votes vote down vote up
Point: React.FC<IProps> = ({ point, handleDelete }: IProps) => {
  return (
    <Container>
      <header>
        {point.image ? (
          <img src={point.image_url} alt={point.name} />
        ) : (
          <img src={noImage} alt={point.name} />
        )}
      </header>
      <section className="body">
        <h2>{point.name}</h2>
        <Contato>
          <h3>Contato</h3>
          <p>{point.email}</p>
          <div>
            <span>
              {point.city}, {point.uf}
            </span>
          </div>
        </Contato>
        <p className="items">
          <strong>
            {point.point_items.map(pi => pi.item.title).join(', ')}
          </strong>
        </p>
      </section>
      <section className="footer">
        <div className="icon-container">
          <Link to={`update-point/${point.id}`} className="icon">
            <FiEdit3 size={20} />
          </Link>

          <button
            type="button"
            className="icon"
            onClick={() => {
              handleDelete(point.id);
            }}
          >
            <FiTrash size={20} />
          </button>
        </div>
      </section>
    </Container>
  );
}
Example #5
Source File: RemoveButton.tsx    From tobira with Apache License 2.0 5 votes vote down vote up
RemoveButton: React.FC<Props> = ({ block: blockRef, onConfirm }) => {
    const { t } = useTranslation();


    const block = useFragment(graphql`
        fragment RemoveButtonData on Block {
            id
        }
    `, blockRef);


    const [commit] = useMutation<RemoveButtonMutation>(graphql`
        mutation RemoveButtonMutation($id: ID!) {
            removeBlock(id: $id) {
                id @deleteRecord
                realm {
                    ... ContentManageRealmData
                }
            }
        }
    `);

    const remove = () => {
        commit({
            variables: block,
            onCompleted: () => {
                currentRef(modalRef).done();
            },
            onError: error => {
                currentRef(modalRef).reportError(
                    displayCommitError(error, t("manage.realm.content.removing-failed")),
                );
            },
        });
    };


    const modalRef = useRef<ConfirmationModalHandle>(null);

    return <>
        <Button
            title={t("manage.realm.content.remove")}
            css={{
                color: "var(--danger-color)",
                "&&:hover": {
                    backgroundColor: "var(--danger-color)",
                    color: "var(--danger-color-bw-contrast)",
                },
            }}
            onClick={() => {
                currentRef(modalRef).open();
                onConfirm?.();
            }}
        >
            <FiTrash />
        </Button>
        <ConfirmationModal
            buttonContent={t("manage.realm.content.remove")}
            onSubmit={remove}
            ref={modalRef}
        >
            <p>
                <Trans i18nKey="manage.realm.danger-zone.delete.cannot-be-undone" />
            </p>
        </ConfirmationModal>
    </>;
}
Example #6
Source File: ContactsLayout.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
ContactsLayout = (): JSX.Element => {
    const [search, setSearch] = useState('' as string);
    const [isLoading, setIsLoading] = useBoolean(true);
    const [contacts, setContacts] = useState([] as any[]);
    const [permission, setPermission] = useState((): string | null => {
        return null;
    });
    const dialogRef = useRef(null);
    const inputFile = useRef(null);
    const [dialogOpen, setDialogOpen] = useBoolean();
    const alertRef = useRef(null);
    const [requiresConfirmation, confirm] = useState((): string | null => {
        return null;
    });

    let filteredContacts = contacts;
    if (search && search.length > 0) {
        filteredContacts = filteredContacts.filter((c) => buildIdentifier(c).includes(search.toLowerCase()));
    }

    const {
        currentPage,
        setCurrentPage,
        pagesCount,
        pages
    } = usePagination({
        pagesCount: Math.ceil(filteredContacts.length / perPage),
        initialState: { currentPage: 1 },
    });

    const refreshPermissionStatus = async (): Promise<void> => {
        setPermission(null);
        await waitMs(500);
        ipcRenderer.invoke('contact-permission-status').then((status: string) => {
            setPermission(status);
        }).catch(() => {
            setPermission('Unknown');
        });
    };

    const requestContactPermission = async (): Promise<void> => {
        setPermission(null);
        ipcRenderer.invoke('request-contact-permission').then((status: string) => {
            setPermission(status);
        }).catch(() => {
            setPermission('Unknown');
        });
    };

    const loadContacts = (showToast = false) => {
        ipcRenderer.invoke('get-contacts').then((contactList: any[]) => {
            setContacts(contactList.map((e: any) => {
                // Patch the ID as a string
                e.id = String(e.id);
                return e;
            }));
            setIsLoading.off();
        }).catch(() => {
            setIsLoading.off();
        });

        if (showToast) {
            showSuccessToast({
                id: 'contacts',
                description: 'Successfully refreshed Contacts!'
            });
        }
    };

    useEffect(() => {
        loadContacts();
        refreshPermissionStatus();
    }, []);

    const getEmptyContent = () => {
        const wrap = (child: JSX.Element) => {
            return (
                <section style={{marginTop: 20}}>
                    {child}
                </section>
            );
        };

        if (isLoading) {
            return wrap(<CircularProgress isIndeterminate />);
        }

        if (contacts.length === 0) {
            return wrap(<Text fontSize="md">BlueBubbles found no contacts in your Mac's Address Book!</Text>);
        }

        return null;
    };

    const filterContacts = () => {
        return filteredContacts.slice((currentPage - 1) * perPage, currentPage * perPage);
    };

    const onCreate = async (contact: ContactItem) => {
        const newContact = await createContact(
            contact.firstName,
            contact.lastName,
            {
                emails: contact.emails.map((e: NodeJS.Dict<any>) => e.address),
                phoneNumbers: contact.phoneNumbers.map((e: NodeJS.Dict<any>) => e.address)
            }
        );

        if (newContact) {
            // Patch the contact using a string ID & source type
            newContact.id = String(newContact.id);
            newContact.sourceType = 'db';

            // Patch the addresses
            (newContact as any).phoneNumbers = (newContact as any).addresses.filter((e: any) => e.type === 'phone');
            (newContact as any).emails = (newContact as any).addresses.filter((e: any) => e.type === 'email');

            setContacts([newContact, ...contacts]);
        }
    };

    const onUpdate = async (contact: NodeJS.Dict<any>) => {
        const cId = typeof(contact.id) === 'string' ? Number.parseInt(contact.id) : contact.id as number;
        const newContact = await updateContact(
            cId,
            {
                firstName: contact.firstName,
                lastName: contact.lastName,
                displayName: contact.displayName
            }
        );

        const copiedContacts = [...contacts];
        let updated = false;
        for (let i = 0; i < copiedContacts.length; i++) {
            if (copiedContacts[i].id === String(cId)) {
                copiedContacts[i].firstName = newContact.firstName;
                copiedContacts[i].lastName = newContact.lastName;
                copiedContacts[i].displayName = newContact.displayName;
                updated = true;
            }
        }

        if (updated) {
            setContacts(copiedContacts);
        }
    };

    const onDelete = async (contactId: number | string) => {
        await deleteContact(typeof(contactId) === 'string' ? Number.parseInt(contactId as string) : contactId);
        setContacts(contacts.filter((e: ContactItem) => {
            return e.id !== String(contactId);
        }));
    };

    const onAddAddress = async (contactId: number | string, address: string) => {
        const cId = typeof(contactId) === 'string' ? Number.parseInt(contactId as string) : contactId;
        const addr = await addAddressToContact(cId, address, address.includes('@') ? 'email' : 'phone');
        if (addr) {
            setContacts(contacts.map((e: ContactItem) => {
                if (e.id !== String(contactId)) return e;
                if (address.includes('@')) {
                    e.emails = [...e.emails, addr];
                } else {
                    e.phoneNumbers = [...e.phoneNumbers, addr];
                }

                return e;
            }));
        }
    };

    const onDeleteAddress = async (contactAddressId: number) => {
        await deleteContactAddress(contactAddressId);
        setContacts(contacts.map((e: ContactItem) => {
            e.emails = e.emails.filter((e: ContactAddress) => e.id !== contactAddressId);
            e.phoneNumbers = e.phoneNumbers.filter((e: ContactAddress) => e.id !== contactAddressId);
            return e;
        }));
    };

    const clearLocalContacts = async () => {
        // Delete the contacts, then filter out the DB items
        await deleteLocalContacts();
        setContacts(contacts.filter(e => e.sourceType !== 'db'));
    };

    const confirmationActions: ConfirmationItems = {
        clearLocalContacts: {
            message: (
                'Are you sure you want to clear/delete all local Contacts?<br /><br />' +
                'This will remove any Contacts added manually, via the API, or via the import process'
            ),
            func: clearLocalContacts
        }
    };

    return (
        <Box p={3} borderRadius={10}>
            <Stack direction='column' p={5}>
                <Text fontSize='2xl'>Controls</Text>
                <Divider orientation='horizontal' />
                <Box>
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"mr={5}
                        >
                            Manage
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<BsPersonPlus />} onClick={() => setDialogOpen.on()}>
                                Add Contact
                            </MenuItem>
                            <MenuItem icon={<BiRefresh />} onClick={() => loadContacts(true)}>
                                Refresh Contacts
                            </MenuItem>
                            <MenuItem
                                icon={<BiImport />}
                                onClick={() => {
                                    if (inputFile && inputFile.current) {
                                        (inputFile.current as HTMLElement).click();
                                    }
                                }}
                            >
                                Import VCF
                                <input
                                    type='file'
                                    id='file'
                                    ref={inputFile}
                                    accept=".vcf"
                                    style={{display: 'none'}}
                                    onChange={(e) => {
                                        const files = e?.target?.files ?? [];
                                        for (const i of files) {
                                            ipcRenderer.invoke('import-vcf', i.webkitRelativePath);
                                        }
                                    }}
                                />
                            </MenuItem>
                            <MenuDivider />
                            <MenuItem icon={<FiTrash />} onClick={() => confirm('clearLocalContacts')}>
                                Clear Local Contacts
                            </MenuItem>
                        </MenuList>
                    </Menu>
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"
                            mr={5}
                        >
                            Permissions
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<BiRefresh />} onClick={() => refreshPermissionStatus()}>
                                Refresh Permission Status
                            </MenuItem>
                            {(permission !== null && permission !== 'Authorized') ? (
                                <MenuItem icon={<BsUnlockFill />} onClick={() => requestContactPermission()}>
                                    Request Permission
                                </MenuItem>
                            ) : null}
                        </MenuList>
                    </Menu>
                    <Text as="span" verticalAlign="middle">
                        Status: <Text as="span" color={getPermissionColor(permission)}>
                            {permission ? permission : 'Checking...'}
                        </Text>
                    </Text>
                </Box>
            </Stack>
            <Stack direction='column' p={5}>
                <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                    <Text fontSize='2xl'>Contacts ({filteredContacts.length})</Text>
                    <Popover trigger='hover'>
                        <PopoverTrigger>
                            <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                <AiOutlineInfoCircle />
                            </Box>
                        </PopoverTrigger>
                        <PopoverContent>
                            <PopoverArrow />
                            <PopoverCloseButton />
                            <PopoverHeader>Information</PopoverHeader>
                            <PopoverBody>
                                <Text>
                                    Here are the contacts on your macOS device that BlueBubbles knows about,
                                    and will serve to any clients that want to know about them. These include
                                    contacts from this Mac's Address Book, as well as contacts from uploads/imports
                                    or manual entry.
                                </Text>
                            </PopoverBody>
                        </PopoverContent>
                    </Popover>
                </Flex>
                <Divider orientation='horizontal' />
                <Flex flexDirection='row' justifyContent='flex-end' alignItems='center' pt={3}>
                    <InputGroup width="xxs">
                        <InputLeftElement pointerEvents='none'>
                            <AiOutlineSearch color='gray.300' />
                        </InputLeftElement>
                        <Input
                            placeholder='Search Contacts'
                            onChange={(e) => {
                                if (currentPage > 1) {
                                    setCurrentPage(1);
                                }

                                setSearch(e.target.value);
                            }}
                            value={search}
                        />
                    </InputGroup>
                </Flex>
                <Flex justifyContent="center" alignItems="center">
                    {getEmptyContent()}
                </Flex>
                {(contacts.length > 0) ? (
                    <ContactsTable
                        contacts={filterContacts()}
                        onCreate={onCreate}
                        onDelete={onDelete}
                        onUpdate={onUpdate}
                        onAddressAdd={onAddAddress}
                        onAddressDelete={onDeleteAddress}
                    />
                ) : null}
                <Pagination
                    pagesCount={pagesCount}
                    currentPage={currentPage}
                    onPageChange={setCurrentPage}
                >
                    <PaginationContainer
                        align="center"
                        justify="space-between"
                        w="full"
                        pt={2}
                    >
                        <PaginationPrevious minWidth={'75px'}>Previous</PaginationPrevious>
                        <Box ml={1}></Box>
                        <PaginationPageGroup flexWrap="wrap" justifyContent="center">
                            {pages.map((page: number) => (
                                <PaginationPage 
                                    key={`pagination_page_${page}`} 
                                    page={page}
                                    my={1}
                                    px={3}
                                    fontSize={14}
                                />
                            ))}
                        </PaginationPageGroup>
                        <Box ml={1}></Box>
                        <PaginationNext minWidth={'50px'}>Next</PaginationNext>
                    </PaginationContainer>
                </Pagination>
            </Stack>

            <ContactDialog
                modalRef={dialogRef}
                isOpen={dialogOpen}
                onCreate={onCreate}
                onDelete={onDelete}
                onAddressAdd={onAddAddress}
                onAddressDelete={onDeleteAddress}
                onClose={() => setDialogOpen.off()}
            />

            <ConfirmationDialog
                modalRef={alertRef}
                onClose={() => confirm(null)}
                body={confirmationActions[requiresConfirmation as string]?.message}
                onAccept={() => {
                    confirmationActions[requiresConfirmation as string].func();
                }}
                isOpen={requiresConfirmation !== null}
            />
        </Box>
    );
}
Example #7
Source File: DevicesLayout.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
DevicesLayout = (): JSX.Element => {
    const [requiresConfirmation, confirm] = useState((): string | null => {
        return null;
    });
    const alertRef = useRef(null);
    const devices = useAppSelector(state => state.deviceStore.devices);
    const dispatch = useAppDispatch();
    
    useEffect(() => {
        refreshDevices(false);

        // Refresh devices every 60 seconds
        const refresher = setInterval(() => {
            refreshDevices(false);
        }, 60000);

        // Return a function to clear the interval on unmount
        return () => clearInterval(refresher);
    }, []);

    return (
        <Box p={3} borderRadius={10}>
            <Stack direction='column' p={5}>
                <Text fontSize='2xl'>Controls</Text>
                <Divider orientation='horizontal' />
                <Box>
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"mr={5}
                        >
                            Manage
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<BiRefresh />} onClick={() => refreshDevices()}>
                                Refresh Devices
                            </MenuItem>
                            <MenuItem icon={<FiTrash />} onClick={() => confirm('clearDevices')}>
                                Clear Devices
                            </MenuItem>
                        </MenuList>
                    </Menu>
                </Box>
            </Stack>
            <Stack direction='column' p={5}>
                <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                    <Text fontSize='2xl'>Devices</Text>
                    <Popover trigger='hover'>
                        <PopoverTrigger>
                            <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                <AiOutlineInfoCircle />
                            </Box>
                        </PopoverTrigger>
                        <PopoverContent>
                            <PopoverArrow />
                            <PopoverCloseButton />
                            <PopoverHeader>Information</PopoverHeader>
                            <PopoverBody>
                                <Text>
                                    Here is where you'll find any devices that are registered with your BlueBubbles
                                    server to receive notifications and other messages. If you do not see your device
                                    here after setting up your app, please contact us for assistance.
                                </Text>
                            </PopoverBody>
                        </PopoverContent>
                    </Popover>
                </Flex>
                <Divider orientation='horizontal' />
                {(devices.length === 0) ? (
                    <Flex justifyContent="center" alignItems="center">
                        <section style={{marginTop: 20}}>
                            <Text fontSize="md">You have no devices registered with the server!</Text>
                        </section>
                    </Flex>
                ) : null}
                {(devices.length > 0) ? (
                    <DevicesTable devices={devices} />
                ) : null}
            </Stack>

            <ConfirmationDialog
                modalRef={alertRef}
                onClose={() => confirm(null)}
                body={confirmationActions[requiresConfirmation as string]?.message}
                onAccept={() => {
                    if (hasKey(confirmationActions, requiresConfirmation as string)) {
                        if (confirmationActions[requiresConfirmation as string].shouldDispatch ?? false) {
                            dispatch(confirmationActions[requiresConfirmation as string].func() as AnyAction);
                        } else {
                            confirmationActions[requiresConfirmation as string].func();
                        }
                    }
                }}
                isOpen={requiresConfirmation !== null}
            />
        </Box>
    );
}
Example #8
Source File: FcmLayout.tsx    From bluebubbles-server with Apache License 2.0 4 votes vote down vote up
FcmLayout = (): JSX.Element => {
    const dispatch = useAppDispatch();
    const alertRef = useRef(null);
    const confirmationActions: NodeJS.Dict<any> = {
        clearConfiguration: {
            message: (
                'Are you sure you want to clear your FCM Configuration?<br /><br />' +
                'Doing so will prevent notifications from being delivered until ' +
                'your configuration is re-loaded'
            ),
            func: async () => {
                const success = await clearFcmConfiguration();
                if (success) {
                    dispatch(setConfig({ name: 'fcm_client', 'value': null }));
                    dispatch(setConfig({ name: 'fcm_server', 'value': null }));
                }
            }
        }
    };

    const serverLoaded = (useAppSelector(state => state.config.fcm_server !== null) ?? false);
    const clientLoaded = (useAppSelector(state => state.config.fcm_client !== null) ?? false);
    const [isDragging, setDragging] = useBoolean();
    const [errors, setErrors] = useState([] as Array<ErrorItem>);
    const [requiresConfirmation, setRequiresConfirmation] = useState(null as string | null);
    const alertOpen = errors.length > 0;
    

    const onDrop = async (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();

        dragCounter = 0;
        setDragging.off();
        
        // I'm not sure why, but we need to copy the file data _before_ we read it using the file reader.
        // If we do not, the data transfer file list gets set to empty after reading the first file.
        const listCopy: Array<Blob> = [];
        for (let i = 0; i < e.dataTransfer.files.length; i++) {
            listCopy.push(e.dataTransfer.files.item(i) as Blob);
        }

        // Actually read the files
        const errors: Array<ErrorItem> = [];
        for (let i = 0; i < listCopy.length; i++) {
            try {
                const fileStr = await readFile(listCopy[i]);
                const validClient = isValidClientConfig(fileStr);
                const validServer = isValidServerConfig(fileStr);
                const jsonData = JSON.parse(fileStr);

                if (validClient) {
                    const test = isValidFirebaseUrl(jsonData);
                    if (test) {
                        await saveFcmClient(jsonData);
                        dispatch(setConfig({ name: 'fcm_client', 'value': jsonData }));
                    } else {
                        throw new Error(
                            'Your Firebase setup does not have a real-time database enabled. ' +
                            'Please enable the real-time database in your Firebase Console.'
                        );
                    }
                } else if (validServer) {
                    await saveFcmServer(jsonData);
                    dispatch(setConfig({ name: 'fcm_server', 'value': jsonData }));
                } else {
                    throw new Error('Invalid Google FCM File!');
                }
            } catch (ex: any) {
                errors.push({ id: String(i), message: ex?.message ?? String(ex) });
            }
        }

        if (errors.length > 0) {
            setErrors(errors);
        }
    };

    const onDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
        e.preventDefault();
        if (dragCounter === 0) {
            setDragging.on();
        }

        dragCounter += 1;
    };

    const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
        e.stopPropagation();
        e.preventDefault();
    };

    const onDragLeave = () => {
        dragCounter -= 1;
        if (dragCounter === 0) {
            setDragging.off();
        }
    };

    const closeAlert = () => {
        setErrors([]);
    };

    const confirm = (confirmationType: string | null) => {
        setRequiresConfirmation(confirmationType);
    };

    return (
        <Box
            p={3}
            borderRadius={10}
            onDragEnter={(e) => onDragEnter(e)}
            onDragLeave={() => onDragLeave()}
            onDragOver={(e) => onDragOver(e)}
            onDrop={(e) => onDrop(e)}
        >
            <Stack direction='column' p={5}>
                <Text fontSize='2xl'>Controls</Text>
                <Divider orientation='horizontal' />
                <Flex flexDirection="row" justifyContent="flex-start">
                    <Menu>
                        <MenuButton
                            as={Button}
                            rightIcon={<BsChevronDown />}
                            width="12em"
                            mr={5}
                        >
                            Manage
                        </MenuButton>
                        <MenuList>
                            <MenuItem icon={<FiTrash />} onClick={() => confirm('clearConfiguration')}>
                                Clear Configuration
                            </MenuItem>
                        </MenuList>
                    </Menu>
                </Flex>
            </Stack>
            <Stack direction='column' p={5}>
                <Flex flexDirection='row' justifyContent='flex-start' alignItems='center'>
                    <Text fontSize='2xl'>Configuration</Text>
                    <Popover trigger='hover'>
                        <PopoverTrigger>
                            <Box ml={2} _hover={{ color: 'brand.primary', cursor: 'pointer' }}>
                                <AiOutlineInfoCircle />
                            </Box>
                        </PopoverTrigger>
                        <PopoverContent>
                            <PopoverArrow />
                            <PopoverCloseButton />
                            <PopoverHeader>Information</PopoverHeader>
                            <PopoverBody>
                                <Text>
                                    Drag and drop your JSON configuration files from your Google Firebase Console. If you
                                    do not have these configuration files. Please go to
                                    <span style={{ color: baseTheme.colors.brand.primary }}>
                                        <Link href='https://bluebubbles.app/install' color='brand.primary' target='_blank'> Our Website </Link>
                                    </span>
                                    to learn how.
                                </Text>
                                <Text>
                                    These configurations enable the BlueBubbles server to send notifications and other
                                    messages to all of the clients via Google FCM. Google Play Services is required
                                    for Android Devices.
                                </Text>
                            </PopoverBody>
                        </PopoverContent>
                    </Popover>
                </Flex>
                <Divider orientation='horizontal' />
                <Spacer />

                <SimpleGrid columns={2} spacing={5}>
                    <DropZone
                        text="Drag n' Drop Google Services JSON"
                        loadedText="Google Services JSON Successfully Loaded!"
                        isDragging={isDragging}
                        isLoaded={clientLoaded}
                    />
                    <DropZone
                        text="Drag n' Drop Admin SDK JSON"
                        loadedText="Admin SDK JSON Successfully Loaded!"
                        isDragging={isDragging}
                        isLoaded={serverLoaded}
                    />
                </SimpleGrid>
            </Stack>

            <ErrorDialog
                errors={errors}
                modalRef={alertRef}
                onClose={() => closeAlert()}
                isOpen={alertOpen}
            />

            <ConfirmationDialog
                modalRef={alertRef}
                onClose={() => confirm(null)}
                body={confirmationActions[requiresConfirmation as string]?.message}
                onAccept={() => {
                    if (hasKey(confirmationActions, requiresConfirmation as string)) {
                        confirmationActions[requiresConfirmation as string].func();
                    }
                }}
                isOpen={requiresConfirmation !== null}
            />
        </Box>
    );
}
Example #9
Source File: index.tsx    From rocketredis with MIT License 4 votes vote down vote up
Connection: React.FC<IConnectionProps> = ({ connection }) => {
  const [currentConnection, setCurrentConnection] = useRecoilState(
    currentConnectionState
  )
  const [currentDatabase, setCurrentDatabase] = useRecoilState(
    currentDatabaseState
  )
  const setCurrentKey = useSetRecoilState(currentKeyState)
  const [databases, setDatabases] = useState<IDatabase[]>([])
  const [connectionLoading, setConnectionLoading] = useState(false)
  const [isConnectionFailed, setIsConnectionFailed] = useState(false)
  const [isEditModalOpen, toggleEditModalOpen] = useToggle(false)
  const [isDeleteModalOpen, toggleDeleteModalOpen] = useToggle(false)
  const { t } = useTranslation('connection')

  const { addToast } = useToast()

  useEffect(() => {
    if (currentConnection) {
      setIsConnectionFailed(false)
    }
  }, [currentConnection])

  const isConnected = useMemo(() => {
    return currentConnection?.name === connection.name
  }, [currentConnection?.name, connection.name])

  const handleConnect = useCallback(async () => {
    if (!isConnected) {
      setConnectionLoading(true)
      setCurrentConnection(undefined)
      setCurrentDatabase(undefined)
      setCurrentKey(undefined)

      try {
        const databases = await loadConnectionDatabases(connection)

        setDatabases(databases)
        setCurrentConnection(connection)
        setCurrentDatabase(undefined)
      } catch {
        setIsConnectionFailed(true)
      } finally {
        setConnectionLoading(false)
      }
    }
  }, [
    connection,
    isConnected,
    setCurrentConnection,
    setCurrentDatabase,
    setCurrentKey
  ])

  const handleDisconnect = useCallback(async () => {
    setCurrentConnection(undefined)
    setCurrentDatabase(undefined)
    terminateConnection()
  }, [setCurrentConnection, setCurrentDatabase])

  const handleRefreshDatabases = useCallback(async () => {
    try {
      setConnectionLoading(true)

      const databases = await loadConnectionDatabases(connection)

      setDatabases(databases)
    } catch {
      setIsConnectionFailed(true)
    } finally {
      setConnectionLoading(false)
    }
  }, [connection])

  const postSavingConnection = useCallback(async () => {
    toggleEditModalOpen()
    setCurrentConnection(undefined)
    setCurrentDatabase(undefined)
    setIsConnectionFailed(false)
  }, [toggleEditModalOpen, setCurrentConnection, setCurrentDatabase])

  const postDeletingConnection = useCallback(async () => {
    toggleDeleteModalOpen()
    setCurrentConnection(undefined)
    setCurrentDatabase(undefined)
    terminateConnection()
  }, [toggleDeleteModalOpen, setCurrentConnection, setCurrentDatabase])

  const handleSelectDatabase = useCallback(
    async (database: IDatabase) => {
      if (!currentConnection) {
        return
      }

      try {
        await initializeConnection(currentConnection, database)

        setCurrentDatabase(database)
        setCurrentKey(undefined)
      } catch {
        addToast({
          type: 'error',
          title: 'Failed to connect to database',
          description:
            'A connection to this Redis database could not be established.'
        })
      }
    },
    [currentConnection, addToast, setCurrentDatabase, setCurrentKey]
  )

  return (
    <>
      <Container
        key={connection.name}
        connected={isConnected}
        errored={isConnectionFailed}
      >
        <ContextMenuTrigger id={`connection_actions_menu:${connection.name}`}>
          <button type="button" disabled={isConnected} onClick={handleConnect}>
            {connectionLoading ? (
              <Loading>
                <FiLoader />
              </Loading>
            ) : (
              <FiDatabase />
            )}
            {connection.name}
            <FiChevronRight />
          </button>
        </ContextMenuTrigger>

        <ContextMenu
          id={`connection_actions_menu:${connection.name}`}
          className="connection-actions-menu"
        >
          {isConnected ? (
            <MenuItem onClick={handleDisconnect}>
              <DisconnectButton>
                <FiMinusCircle />
                {t('contextMenu.disconnect')}
              </DisconnectButton>
            </MenuItem>
          ) : (
            <MenuItem onClick={handleConnect}>
              <ConnectButton>
                <FiActivity />
                {t('contextMenu.connect')}
              </ConnectButton>
            </MenuItem>
          )}

          <MenuItem onClick={toggleEditModalOpen}>
            <FiEdit2 />
            {t('contextMenu.editSettings')}
          </MenuItem>

          {isConnected && (
            <MenuItem onClick={handleRefreshDatabases}>
              <FiRefreshCcw />
              {t('contextMenu.refreshDatabases')}
            </MenuItem>
          )}

          <MenuItem onClick={toggleDeleteModalOpen}>
            <FiTrash />
            {t('contextMenu.deleteConnection')}
          </MenuItem>
        </ContextMenu>

        {isConnected && !!databases.length && (
          <DatabaseList>
            {databases.map(database => (
              <Database
                connected={currentDatabase?.name === database.name}
                key={database.name}
                onClick={() => handleSelectDatabase(database)}
                type="button"
              >
                <strong>{database.name}</strong>
                <span>
                  {database.keys} {t('keys')}
                </span>
              </Database>
            ))}
          </DatabaseList>
        )}

        {isConnectionFailed && (
          <ConnectionError>
            {t('connectionFailed')}{' '}
            <button type="button" onClick={handleConnect}>
              {t('retry')}
            </button>
          </ConnectionError>
        )}
      </Container>

      <ConnectionFormModal
        visible={isEditModalOpen}
        onRequestClose={postSavingConnection}
        connectionToEdit={connection}
      />

      <DeleteConnectionModal
        visible={isDeleteModalOpen}
        onRequestClose={postDeletingConnection}
        connectionToDelete={connection}
      />
    </>
  )
}
Example #10
Source File: index.tsx    From rocketredis with MIT License 4 votes vote down vote up
DeleteConnectionModal: React.FC<ConnectionModalProps> = ({
  visible,
  onRequestClose,
  connectionToDelete
}) => {
  const formRef = useRef<FormHandles>(null)
  const { t } = useTranslation('deleteConnection')
  const { addToast } = useToast()
  const setConnections = useSetRecoilState(connectionsState)

  const [deleteConnectionLoading, toggleDeleteConnectionLoading] = useToggle(
    false
  )

  const handleCloseModal = useCallback(() => {
    if (onRequestClose) {
      onRequestClose()
    }
  }, [onRequestClose])

  const handleDeleteConnection = useCallback(
    async ({ confirmation: deleteConfirmation }: DeleteConnectionFormData) => {
      try {
        toggleDeleteConnectionLoading()

        formRef.current?.setErrors({})

        if (deleteConfirmation !== 'DELETE') {
          formRef.current?.setErrors({
            confirmation: t('form.unconfirmedDeletionError')
          })

          return
        }

        const connections = deleteAndGetConnections(connectionToDelete)

        setConnections(connections)

        addToast({
          type: 'success',
          title: 'Connection deleted',
          description: 'This connection will not be available anymore.'
        })

        handleCloseModal()
      } catch (error) {
        addToast({
          type: 'error',
          title: 'Error deleting connection',
          description: error.message || 'Unexpected error occurred, try again.'
        })
      } finally {
        toggleDeleteConnectionLoading()
      }
    },
    [
      toggleDeleteConnectionLoading,
      t,
      addToast,
      connectionToDelete,
      setConnections,
      handleCloseModal
    ]
  )

  return (
    <Modal visible={visible} onRequestClose={onRequestClose}>
      <h1>{t('title')}</h1>

      <TextContent>
        <p>
          <Trans
            t={t}
            i18nKey="deletingConnectionMessage"
            values={{ name: connectionToDelete.name }}
          >
            The connection <strong>{name}</strong> will be permanently deleted.
          </Trans>
        </p>

        <p>
          <Trans t={t} i18nKey="confirmDeletionMessage" />
        </p>
      </TextContent>

      <Form ref={formRef} onSubmit={handleDeleteConnection}>
        <Input name="confirmation" />

        <ActionsContainer>
          <ButtonGroup>
            <Button onClick={handleCloseModal} type="button" color="opaque">
              {t('form.cancel')}
            </Button>

            <Button loading={deleteConnectionLoading} type="submit" color="red">
              <FiTrash />
              {t('form.confirmDeletion')}
            </Button>
          </ButtonGroup>
        </ActionsContainer>
      </Form>
    </Modal>
  )
}
Example #11
Source File: Logs.tsx    From meshtastic-web with GNU General Public License v3.0 4 votes vote down vote up
Logs = (): JSX.Element => {
  const dispatch = useAppDispatch();

  const meshtasticState = useAppSelector((state) => state.meshtastic);
  const appState = useAppSelector((state) => state.app);

  type lookupType = { [key: number]: string };

  const emitterLookup: lookupType = {
    [Types.Emitter.sendText]: 'text-rose-500',
    [Types.Emitter.sendPacket]: 'text-pink-500',
    [Types.Emitter.sendRaw]: 'text-fuchsia-500',
    [Types.Emitter.setConfig]: 'text-purple-500',
    [Types.Emitter.confirmSetConfig]: 'text-violet-500',
    [Types.Emitter.setOwner]: 'text-indigo-500',
    [Types.Emitter.setChannel]: 'text-blue-500',
    [Types.Emitter.confirmSetChannel]: 'text-sky-500',
    [Types.Emitter.deleteChannel]: 'text-cyan-500',
    [Types.Emitter.getChannel]: 'text-teal-500',
    [Types.Emitter.getAllChannels]: 'text-emerald-500',
    [Types.Emitter.getConfig]: 'text-green-500',
    [Types.Emitter.getOwner]: 'text-lime-500',
    [Types.Emitter.configure]: 'text-yellow-500',
    [Types.Emitter.handleFromRadio]: 'text-amber-500',
    [Types.Emitter.handleMeshPacket]: 'text-orange-500',
    [Types.Emitter.connect]: 'text-red-500',
    [Types.Emitter.ping]: 'text-stone-500',
    [Types.Emitter.readFromRadio]: 'text-zinc-500',
    [Types.Emitter.writeToRadio]: 'text-gray-500',
    [Types.Emitter.setDebugMode]: 'text-slate-500',
  };

  const levelLookup: lookupType = {
    [Protobuf.LogRecord_Level.UNSET]: 'text-green-500',
    [Protobuf.LogRecord_Level.CRITICAL]: 'text-purple-500',
    [Protobuf.LogRecord_Level.ERROR]: 'text-red-500',
    [Protobuf.LogRecord_Level.WARNING]: 'text-orange-500',
    [Protobuf.LogRecord_Level.INFO]: 'text-blue-500',
    [Protobuf.LogRecord_Level.DEBUG]: 'text-neutral-500',
    [Protobuf.LogRecord_Level.TRACE]: 'text-slate-500',
  };

  return (
    <div className="flex h-full flex-col gap-4 p-4">
      <Card
        title="Device Logs"
        actions={
          <IconButton
            icon={<FiTrash />}
            onClick={(): void => {
              dispatch(clearLogs());
            }}
          />
        }
        className="flex-grow overflow-y-auto"
      >
        <table className="table-cell flex-grow">
          <tbody
            className="
          block h-full flex-col overflow-y-auto font-mono text-xs dark:text-gray-400"
          >
            <AnimatePresence>
              {meshtasticState.logs.length === 0 && (
                <div className="flex h-full w-full">
                  <m.img
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    exit={{ opacity: 0 }}
                    className="m-auto h-64 w-64 text-green-500"
                    src={
                      appState.darkMode
                        ? '/View_Code_Dark.svg'
                        : '/View_Code.svg'
                    }
                  />
                </div>
              )}

              {meshtasticState.logs.map((log, index) => (
                <m.tr
                  key={index}
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                  transition={{ duration: 0.3 }}
                  className="group hover:bg-gray-300 dark:hover:bg-secondaryDark"
                >
                  <m.td
                    className="w-6 cursor-pointer"
                    whileHover={{ scale: 1.01 }}
                    whileTap={{ scale: 0.99 }}
                  >
                    <div className="m-auto pl-2 text-white group-hover:text-black dark:text-primaryDark dark:group-hover:text-gray-400">
                      <FiArrowRight />
                    </div>
                  </m.td>
                  <Wrapper>
                    {log.date
                      .toLocaleString(undefined, {
                        year: 'numeric',
                        month: '2-digit',
                        day: '2-digit',

                        hour: '2-digit',
                        minute: '2-digit',
                        second: '2-digit',
                      })
                      .replaceAll('/', '-')
                      .replace(',', '')}
                  </Wrapper>
                  <Wrapper>
                    <div className={emitterLookup[log.emitter]}>
                      [{Types.EmitterScope[log.scope]}.
                      {Types.Emitter[log.emitter]}]
                    </div>
                  </Wrapper>
                  <Wrapper className={levelLookup[log.level]}>
                    [{Protobuf.LogRecord_Level[log.level]}]{/* </div> */}
                  </Wrapper>
                  <td
                    className={`m-auto ${
                      log.packet ? '' : 'dark:text-secondaryDark'
                    }`}
                  >
                    <FiPaperclip />
                  </td>
                  <td className="w-full truncate pl-1">{log.message}</td>
                </m.tr>
                // </ContextMenu>
              ))}
            </AnimatePresence>
          </tbody>
        </table>
      </Card>
    </div>
  );
}