Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ module.exports = {
{ allowConstantExport: true },
],
"no-console": "off",
"no-underscore-dangle": "off",
},
};
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ManageProductionsPage } from "./components/manage-productions-page/mana
import { CreateProductionPage } from "./components/create-production/create-production-page.tsx";
import { useSetupTokenRefresh } from "./hooks/use-reauth.tsx";
import { TUserSettings } from "./components/user-settings/types";
import { IngestsPage } from "./components/ingests-page/ingests-page.tsx";

const DisplayBoxPositioningContainer = styled(FlexContainer)`
justify-content: center;
Expand Down Expand Up @@ -152,6 +153,13 @@ const AppContent = ({
}
errorElement={<ErrorPage />}
/>
<Route
path="/ingests"
element={
<IngestsPage setApiError={() => setApiError(true)} />
}
errorElement={<ErrorPage />}
/>
<Route
path="/production-calls/production/:productionId/line/:lineId"
element={<CallsPage />}
Expand Down
77 changes: 77 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ export type TBasicProductionResponse = {
lines: TLine[];
};

export type TIngest = {
_id: string;
label?: string;
ipAddress?: string;
deviceInput?: {
name: string;
label: string;
}[];
deviceOutput?: {
name: string;
label: string;
}[];
};

export type TListIngestResponse = {
ingests: TIngest[];
offset: 0;
limit: 0;
totalItems: 0;
};

export type TListProductionsResponse = {
productions: TBasicProductionResponse[];
offset: 0;
Expand Down Expand Up @@ -291,4 +312,60 @@ export const API = {
})
);
},
fetchIngestList: (): Promise<TListIngestResponse> =>
handleFetchRequest<TListIngestResponse>(
fetch(`${API_URL}ingest`, {
method: "GET",
headers: {
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
})
),
createIngest: async (data: { label: string; ipAddress: string }) =>
handleFetchRequest<boolean>(
fetch(`${API_URL}ingest/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
body: JSON.stringify({
label: data.label,
ipAddress: data.ipAddress,
}),
})
),
fetchIngest: (id: number): Promise<TIngest> =>
handleFetchRequest<TIngest>(
fetch(`${API_URL}ingest/${id}`, {
method: "GET",
headers: {
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
})
),
updateIngest: async (data: TIngest) =>
handleFetchRequest<TIngest>(
fetch(`${API_URL}ingest/${data._id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
body: JSON.stringify({
label: data.label,
deviceInput: data.deviceInput?.[0],
deviceOutput: data.deviceOutput?.[0],
}),
})
),
deleteIngest: async (id: string): Promise<string> =>
handleFetchRequest<string>(
fetch(`${API_URL}ingest/${id}`, {
method: "DELETE",
headers: {
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
},
})
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { SubmitHandler, useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import { useSubmitOnEnter } from "../../../hooks/use-submit-form-enter-press";
import { ButtonWrapper } from "../../generic-components";
import { FormInput } from "../../form-elements/form-elements";
import { FormItem } from "../../user-settings-form/form-item";
import { FormWrapper, SubmitButton } from "../ingest-components";
import { useCreateIngest } from "./use-create-ingest";
import { Spinner } from "../../loader/loader";
import { SpinnerWrapper } from "../../delete-button/delete-button-components";
import { FormValues } from "../types";

type AddIngestFormProps = {
onSave?: () => void;
};

export const AddIngestForm = ({ onSave }: AddIngestFormProps) => {
const [createIngest, setCreateIngest] = useState<FormValues | null>(null);
const {
formState: { errors, isValid },
register,
handleSubmit,
} = useForm<FormValues>({
resetOptions: {
keepDirtyValues: true, // user-interacted input will be retained
keepErrors: true, // input errors will be retained with value update
},
});

const { loading, success } = useCreateIngest({ createIngest });

useEffect(() => {
if (success) {
setCreateIngest(null);
if (onSave) onSave();
}
}, [success, onSave]);

const onSubmit: SubmitHandler<FormValues> = (data) => {
setCreateIngest(data);
};

useSubmitOnEnter<FormValues>({
handleSubmit,
submitHandler: onSubmit,
shouldSubmitOnEnter: true,
});

return (
<FormWrapper>
<FormItem label="Name" fieldName="ingestLabel" errors={errors}>
<FormInput
// eslint-disable-next-line
{...register(`ingestLabel`, {
required: "Ingest name is required",
minLength: 1,
})}
placeholder="Name for Ingest"
/>
</FormItem>
<FormItem label="Server IP Address" fieldName="ipAddress" errors={errors}>
<FormInput
// eslint-disable-next-line
{...register(`ipAddress`, {
required: "IP address is required",
minLength: 1,
})}
placeholder="192.168.1.1"
/>
</FormItem>
<ButtonWrapper>
<SubmitButton
type="button"
disabled={!isValid}
onClick={handleSubmit(onSubmit)}
shouldSubmitOnEnter
>
Add Ingest
{loading && (
<SpinnerWrapper>
<Spinner className="production-list" />
</SpinnerWrapper>
)}
</SubmitButton>
</ButtonWrapper>
</FormWrapper>
);
};
21 changes: 21 additions & 0 deletions src/components/ingests-page/add-ingest-modal/ingest-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FC } from "react";
import { DisplayContainerHeader } from "../../landing-page/display-container-header";
import { ResponsiveFormContainer } from "../../generic-components";
import { AddIngestForm } from "./add-ingest-form";

interface IngestFormModalProps {
className?: string;
onSave?: () => void;
}

export const IngestFormModal: FC<IngestFormModalProps> = (props) => {
const { className, onSave } = props;

return (
<ResponsiveFormContainer className={className}>
<DisplayContainerHeader>Add New Ingest</DisplayContainerHeader>

<AddIngestForm onSave={onSave} />
</ResponsiveFormContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { API } from "../../../api/api";
import { useRequest } from "../../../hooks/use-request";
import { FormValues } from "../types";

export const useCreateIngest = ({
createIngest,
}: {
createIngest: FormValues | null;
}) => {
return useRequest<{ label: string; ipAddress: string }, boolean>({
params: createIngest
? {
label: createIngest.ingestLabel,
ipAddress: createIngest.ipAddress,
}
: null,
apiCall: API.createIngest,
errorMessage: (i) => `Failed to create ingest: ${i.label}`,
});
};
134 changes: 134 additions & 0 deletions src/components/ingests-page/ingest-components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import styled from "@emotion/styled";
import { PrimaryButton } from "../form-elements/form-elements";

export const HeaderWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 1rem;
`;

export const HeaderText = styled.div`
font-size: 2rem;
font-weight: bold;
margin-right: 0.5rem;

.production-name-container {
display: inline-block;
width: 100%;
}
`;

export const Text = styled.p`
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 1rem;
font-weight: bold;
font-size: 1.5rem;
font-weight: 300;
line-height: 3.2rem;
`;

export const Wrapper = styled.div`
display: flex;
flex-direction: column;
gap: 2rem;
margin-top: 1rem;
`;

export const NoDevices = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #888;
font-size: 0.9rem;
text-transform: uppercase;
padding: 1rem;
`;

export const DeviceWrapper = styled.div`
background-color: #2a2a2a;
border-radius: 0.5rem;
overflow: hidden;
`;

export const DeviceSection = styled.div`
margin-bottom: 2rem;
`;

export const DeviceTable = styled.div`
background-color: #2a2a2a;
border-radius: 0.5rem;
overflow: hidden;
`;

export const DeviceTableHeader = styled.div`
display: grid;
grid-template-columns: 1fr 1fr auto auto;
background-color: #1a1a1a;
padding: 0.75rem 1rem;
border-bottom: 1px solid #404040;
`;

export const DeviceTableHeaderCell = styled.div`
color: #888;
font-size: 0.9rem;
font-weight: 500;
text-transform: uppercase;
`;

export const DeviceTableRow = styled.div`
display: grid;
grid-template-columns: 1fr 1fr auto auto;
padding: 0.75rem 1rem;
border-bottom: 1px solid #404040;
align-items: center;

&:last-child {
border-bottom: none;
}

&:hover {
background-color: #333;
}
`;

export const DeviceTableCell = styled.div`
color: white;
font-size: 0.95rem;
margin-right: 1rem;
`;

export const FormWrapper = styled.div`
display: flex;
flex-direction: column;
width: 40rem;
`;

export const ListWrapper = styled.div`
display: flex;
flex-wrap: wrap;
padding: 0 0 0 2rem;
align-items: flex-start;
`;

export const SubmitButton = styled(PrimaryButton)<{
shouldSubmitOnEnter?: boolean;
}>`
outline: ${({ shouldSubmitOnEnter }) =>
shouldSubmitOnEnter ? "2px solid #007bff" : "none"};
outline-offset: ${({ shouldSubmitOnEnter }) =>
shouldSubmitOnEnter ? "2px" : "0"};
`;

export const StatusDot = styled.div<{ isActive: boolean }>`
width: 1rem;
height: 1rem;
border-radius: 50%;
background-color: ${({ isActive }) => (isActive ? "#22c55e" : "#ef4444")};
margin-right: 1rem;
`;
Loading
Loading