mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #44330, Resolves #44331 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests. (I'd defer integration tests to a separate PR since this one is pretty large already.) - [x] QA'd all new/changed functionality manually. I've tested this on both the setup flow and the organization settings page. I haven't had the time to test this on other places where we render the logo (macOS setup experience / MDM migration dialog). https://github.com/user-attachments/assets/95d4eae5-3da6-40f4-98a1-8575b97d96b3 ## New Fleet configuration settings - [x] Setting(s) is/are explicitly excluded from GitOps. Will handle GitOps in a separate PR. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Organizations can upload custom logos for light and dark modes. * Registration and Org Settings support logo file upload, preview, per-mode replace/delete, and validation (size & image formats). * Activity feed records logo changes/deletions; site nav displays uploaded logos per theme. * File uploader/preview adds a Fleet logo graphic option and improved logo validation. * Config/GitOps outputs now include separate dark/light logo fields. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
import React, { useEffect, useRef, useState } from "react";
|
|
|
|
import {
|
|
ORG_LOGO_ACCEPT,
|
|
ORG_LOGO_HELP_TEXT,
|
|
validateOrgLogoFile,
|
|
} from "utilities/file/orgLogoFile";
|
|
|
|
import Button from "components/buttons/Button";
|
|
import FileUploader from "components/FileUploader";
|
|
import InputField from "components/forms/fields/InputField";
|
|
|
|
interface IOrgDetailsFormData {
|
|
org_name: string;
|
|
org_logo_file?: File | null;
|
|
}
|
|
|
|
interface IOrgDetailsErrors {
|
|
org_name?: string;
|
|
org_logo_file?: string;
|
|
}
|
|
|
|
interface IOrgDetailsProps {
|
|
className?: string;
|
|
currentPage: boolean;
|
|
formData?: Partial<IOrgDetailsFormData>;
|
|
handleSubmit: (formData: IOrgDetailsFormData) => void;
|
|
}
|
|
|
|
interface ICustomLogo {
|
|
file: File;
|
|
url: string;
|
|
}
|
|
|
|
const OrgDetails = ({
|
|
className,
|
|
currentPage,
|
|
formData,
|
|
handleSubmit,
|
|
}: IOrgDetailsProps) => {
|
|
const [orgName, setOrgName] = useState<string>(formData?.org_name || "");
|
|
const [customLogo, setCustomLogo] = useState<ICustomLogo | null>(() => {
|
|
if (!formData?.org_logo_file) return null;
|
|
return {
|
|
file: formData.org_logo_file,
|
|
url: URL.createObjectURL(formData.org_logo_file),
|
|
};
|
|
});
|
|
const [errors, setErrors] = useState<IOrgDetailsErrors>({});
|
|
|
|
// Revoke every live blob URL on unmount
|
|
const activePreviewUrlRef = useRef<string | null>(customLogo?.url ?? null);
|
|
useEffect(() => {
|
|
activePreviewUrlRef.current = customLogo?.url ?? null;
|
|
}, [customLogo]);
|
|
useEffect(
|
|
() => () => {
|
|
if (activePreviewUrlRef.current) {
|
|
URL.revokeObjectURL(activePreviewUrlRef.current);
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
const onOrgNameChange = (value: string) => {
|
|
setOrgName(value);
|
|
setErrors((prev) => ({ ...prev, org_name: undefined }));
|
|
};
|
|
|
|
const onFileSelect = async (files: FileList | null) => {
|
|
if (!files || files.length === 0) return;
|
|
const file = files[0];
|
|
const result = await validateOrgLogoFile(file);
|
|
if (!result.valid) {
|
|
setErrors((prev) => ({ ...prev, org_logo_file: result.error }));
|
|
return;
|
|
}
|
|
setErrors((prev) => ({ ...prev, org_logo_file: undefined }));
|
|
const url = URL.createObjectURL(file);
|
|
setCustomLogo((prev) => {
|
|
if (prev) URL.revokeObjectURL(prev.url);
|
|
return { file, url };
|
|
});
|
|
};
|
|
|
|
const onDeleteFile = () => {
|
|
setCustomLogo((prev) => {
|
|
if (prev) URL.revokeObjectURL(prev.url);
|
|
return null;
|
|
});
|
|
setErrors((prev) => ({ ...prev, org_logo_file: undefined }));
|
|
};
|
|
|
|
const onSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
|
|
evt.preventDefault();
|
|
if (!orgName) {
|
|
setErrors((prev) => ({
|
|
...prev,
|
|
org_name: "Organization name must be present",
|
|
}));
|
|
return;
|
|
}
|
|
handleSubmit({
|
|
org_name: orgName,
|
|
org_logo_file: customLogo?.file ?? null,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={onSubmit} className={className} autoComplete="off">
|
|
<InputField
|
|
label="Organization name"
|
|
name="org_name"
|
|
value={orgName}
|
|
onChange={onOrgNameChange}
|
|
error={errors.org_name}
|
|
autofocus={!!currentPage}
|
|
/>
|
|
<FileUploader
|
|
label="Organization logo (optional)"
|
|
graphicName="fleet-logo"
|
|
accept={ORG_LOGO_ACCEPT}
|
|
message={ORG_LOGO_HELP_TEXT}
|
|
buttonMessage="Choose file"
|
|
onFileUpload={onFileSelect}
|
|
onDeleteFile={onDeleteFile}
|
|
canEdit
|
|
fileDetails={customLogo ? { name: customLogo.file.name } : undefined}
|
|
customPreview={
|
|
customLogo ? (
|
|
<img
|
|
src={customLogo.url}
|
|
alt="Organization logo preview"
|
|
width={40}
|
|
height={40}
|
|
style={{ objectFit: "contain" }}
|
|
/>
|
|
) : undefined
|
|
}
|
|
internalError={errors.org_logo_file}
|
|
/>
|
|
<div className="button-wrap--center">
|
|
<Button type="submit" disabled={!currentPage} size="wide">
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export default OrgDetails;
|