diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py index f46f290fe..ee3ae1206 100644 --- a/backend/app/deps/authorization_deps.py +++ b/backend/app/deps/authorization_deps.py @@ -5,7 +5,7 @@ from app.keycloak_auth import get_current_username from app.models.authorization import RoleType, AuthorizationDB -from app.models.datasets import DatasetDB +from app.models.datasets import DatasetDB, DatasetStatus from app.models.files import FileOut, FileDB from app.models.groups import GroupOut, GroupDB from app.models.metadata import MetadataDB @@ -40,6 +40,24 @@ async def get_role_by_file( AuthorizationDB.user_ids == current_user, ), ) + if authorization is None: + if ( + dataset := await DatasetDB.get(PydanticObjectId(file.dataset_id)) + ) is not None: + if dataset.status == DatasetStatus.AUTHENTICATED.name: + auth_dict = { + "creator": dataset.author.email, + "dataset_id": file.dataset_id, + "user_ids": [current_user], + "role": RoleType.VIEWER, + } + authenticated_auth = AuthorizationDB(**auth_dict) + return authenticated_auth + else: + raise HTTPException( + status_code=403, + detail=f"User `{current_user} does not have role on file {file_id}", + ) return authorization.role raise HTTPException(status_code=404, detail=f"File {file_id} not found") @@ -96,6 +114,28 @@ async def get_role_by_group( raise HTTPException(status_code=404, detail=f"Group {group_id} not found") +async def is_public_dataset( + dataset_id: str, +) -> bool: + """Checks if a dataset is public.""" + if (dataset_out := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: + if dataset_out.status == DatasetStatus.PUBLIC: + return True + else: + return False + + +async def is_authenticated_dataset( + dataset_id: str, +) -> bool: + """Checks if a dataset is authenticated.""" + if (dataset_out := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: + if dataset_out.status == DatasetStatus.AUTHENTICATED: + return True + else: + return False + + class Authorization: """We use class dependency so that we can provide the `permission` parameter to the dependency. For more info see https://fastapi.tiangolo.com/advanced/advanced-dependencies/.""" @@ -125,10 +165,24 @@ async def __call__( detail=f"User `{current_user} does not have `{self.role}` permission on dataset {dataset_id}", ) else: - raise HTTPException( - status_code=403, - detail=f"User `{current_user} does not have `{self.role}` permission on dataset {dataset_id}", - ) + if ( + current_dataset := await DatasetDB.get(PydanticObjectId(dataset_id)) + ) is not None: + if ( + current_dataset.status == DatasetStatus.AUTHENTICATED.name + and self.role == "viewer" + ): + return True + else: + raise HTTPException( + status_code=403, + detail=f"User `{current_user} does not have `{self.role}` permission on dataset {dataset_id}", + ) + else: + raise HTTPException( + status_code=404, + detail=f"The dataset {dataset_id} is not found", + ) class FileAuthorization: @@ -251,6 +305,52 @@ async def __call__( raise HTTPException(status_code=404, detail=f"Group {group_id} not found") +class CheckStatus: + """We use class dependency so that we can provide the `permission` parameter to the dependency. + For more info see https://fastapi.tiangolo.com/advanced/advanced-dependencies/.""" + + def __init__(self, status: str): + self.status = status + + async def __call__( + self, + dataset_id: str, + ): + if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: + if dataset.status == self.status: + return True + else: + return False + else: + return False + + +class CheckFileStatus: + """We use class dependency so that we can provide the `permission` parameter to the dependency. + For more info see https://fastapi.tiangolo.com/advanced/advanced-dependencies/.""" + + def __init__(self, status: str): + self.status = status + + async def __call__( + self, + file_id: str, + ): + if (file_out := await FileDB.get(PydanticObjectId(file_id))) is not None: + dataset_id = file_out.dataset_id + if ( + dataset := await DatasetDB.get(PydanticObjectId(dataset_id)) + ) is not None: + if dataset.status == self.status: + return True + else: + return False + else: + return False + else: + return False + + def access(user_role: RoleType, role_required: RoleType) -> bool: """Enforce implied role hierarchy OWNER > EDITOR > UPLOADER > VIEWER""" if user_role == RoleType.OWNER: diff --git a/backend/app/heartbeat_listener_sync.py b/backend/app/heartbeat_listener_sync.py new file mode 100644 index 000000000..f975b53c5 --- /dev/null +++ b/backend/app/heartbeat_listener_sync.py @@ -0,0 +1,128 @@ +import logging +import pika +import json +from packaging import version +from pymongo import MongoClient + +from app.config import settings +from app.models.search import SearchCriteria +from app.routers.feeds import FeedIn, FeedListener, FeedOut, FeedDB, associate_listener +from app.models.listeners import EventListenerDB, EventListenerOut, ExtractorInfo + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def callback(ch, method, properties, body): + """This method receives messages from RabbitMQ and processes them. + the extractor info is parsed from the message and if the extractor is new + or is a later version, the db is updated. + """ + msg = json.loads(body.decode("utf-8")) + + extractor_info = msg["extractor_info"] + extractor_name = extractor_info["name"] + extractor_db = EventListenerDB( + **extractor_info, properties=ExtractorInfo(**extractor_info) + ) + + mongo_client = MongoClient(settings.MONGODB_URL) + db = mongo_client[settings.MONGO_DATABASE] + + # check to see if extractor alredy exists + existing_extractor = db["listeners"].find_one({"name": msg["queue"]}) + if existing_extractor is not None: + # Update existing listener + existing_version = existing_extractor["version"] + new_version = extractor_db.version + if version.parse(new_version) > version.parse(existing_version): + # if this is a new version, add it to the database + new_extractor = db["listeners"].insert_one(extractor_db.to_mongo()) + found = db["listeners"].find_one({"_id": new_extractor.inserted_id}) + # TODO - for now we are not deleting an older version of the extractor, just adding a new one + # removed = db["listeners"].delete_one({"_id": existing_extractor["_id"]}) + extractor_out = EventListenerOut.from_mongo(found) + logger.info( + "%s updated from %s to %s" + % (extractor_name, existing_version, new_version) + ) + return extractor_out + else: + # Register new listener + new_extractor = db["listeners"].insert_one(extractor_db.to_mongo()) + found = db["listeners"].find_one({"_id": new_extractor.inserted_id}) + extractor_out = EventListenerOut.from_mongo(found) + logger.info("New extractor registered: " + extractor_name) + + # Assign MIME-based listener if needed + if extractor_out.properties and extractor_out.properties.process: + process = extractor_out.properties.process + if "file" in process: + # Create a MIME-based feed for this v1 extractor + criteria_list = [] + for mime in process["file"]: + main_type = mime.split("/")[0] if mime.find("/") > -1 else mime + sub_type = mime.split("/")[1] if mime.find("/") > -1 else None + if sub_type: + if sub_type == "*": + # If a wildcard, just match on main type + criteria_list.append( + SearchCriteria( + field="content_type_main", value=main_type + ) + ) + else: + # Otherwise match the whole string + criteria_list.append( + SearchCriteria(field="content_type", value=mime) + ) + else: + criteria_list.append( + SearchCriteria(field="content_type", value=mime) + ) + + # TODO: Who should the author be for an auto-generated feed? Currently None. + new_feed = FeedDB( + name=extractor_name, + search={ + "index_name": "file", + "criteria": criteria_list, + "mode": "or", + }, + listeners=[ + FeedListener(listener_id=extractor_out.id, automatic=True) + ], + ) + db["feeds"].insert_one(new_feed.to_mongo()) + + return extractor_out + + +def listen_for_heartbeats(): + """ + + this method runs continuously listening for extractor heartbeats send over rabbitmq + + """ + credentials = pika.PlainCredentials(settings.RABBITMQ_USER, settings.RABBITMQ_PASS) + parameters = pika.ConnectionParameters( + settings.RABBITMQ_HOST, 5672, "/", credentials + ) + connection = pika.BlockingConnection(parameters) + channel = connection.channel() + + channel.exchange_declare( + exchange=settings.HEARTBEAT_EXCHANGE, exchange_type="fanout", durable=True + ) + result = channel.queue_declare(queue="", exclusive=True) + queue_name = result.method.queue + channel.queue_bind(exchange=settings.HEARTBEAT_EXCHANGE, queue=queue_name) + + logger.info(" [*] Waiting for heartbeats. To exit press CTRL+C") + channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True) + channel.start_consuming() + + +if __name__ == "__main__": + listen_for_heartbeats() diff --git a/backend/app/main.py b/backend/app/main.py index 0004c84ee..505bec660 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -122,10 +122,7 @@ dependencies=[Depends(get_current_username)], ) api_router.include_router( - metadata_datasets.router, - prefix="/datasets", - tags=["metadata"], - dependencies=[Depends(get_current_username)], + metadata_datasets.router, prefix="/datasets", tags=["metadata"] ) api_router.include_router( folders.router, diff --git a/backend/app/models/datasets.py b/backend/app/models/datasets.py index 6f3a17949..b898dbb72 100644 --- a/backend/app/models/datasets.py +++ b/backend/app/models/datasets.py @@ -19,6 +19,7 @@ def _generate_next_value_(name, start, count, last_values): class DatasetStatus(AutoName): PRIVATE = auto() PUBLIC = auto() + AUTHENTICATED = auto() DEFAULT = auto() TRIAL = auto() @@ -35,6 +36,7 @@ class DatasetIn(DatasetBase): class DatasetPatch(BaseModel): name: Optional[str] description: Optional[str] + status: Optional[str] class DatasetDB(Document, DatasetBase): @@ -63,6 +65,7 @@ class DatasetDBViewList(View, DatasetBase): modified: datetime = Field(default_factory=datetime.utcnow) auth: List[AuthorizationDB] thumbnail_id: Optional[PydanticObjectId] = None + status: Optional[str] class Settings: source = DatasetDB diff --git a/backend/app/routers/authorization.py b/backend/app/routers/authorization.py index 988d6dfd5..951b4b68d 100644 --- a/backend/app/routers/authorization.py +++ b/backend/app/routers/authorization.py @@ -3,7 +3,6 @@ from bson import ObjectId from fastapi import APIRouter, Depends from fastapi.exceptions import HTTPException - from app.dependencies import get_elasticsearchclient from app.deps.authorization_deps import ( Authorization, @@ -25,6 +24,7 @@ DatasetRoles, DatasetDB, DatasetOut, + DatasetStatus, ) from app.models.groups import GroupDB from app.models.pyobjectid import PyObjectId @@ -80,9 +80,23 @@ async def get_dataset_role( ), ) ) is None: - raise HTTPException( - status_code=404, detail=f"No authorization found for dataset: {dataset_id}" - ) + if ( + current_dataset := await DatasetDB.get(PydanticObjectId(dataset_id)) + ) is not None: + if current_dataset.status == DatasetStatus.AUTHENTICATED.name: + public_authorization_in = { + "dataset_id": PydanticObjectId(dataset_id), + "role": RoleType.VIEWER, + } + authorization = AuthorizationDB( + **public_authorization_in, creator=current_dataset.creator.email + ) + return authorization.dict() + else: + raise HTTPException( + status_code=404, + detail=f"No authorization found for dataset: {dataset_id}", + ) else: return auth_db.dict() diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index a97d28d76..feae96e5a 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -32,7 +32,7 @@ from app import dependencies from app.config import settings -from app.deps.authorization_deps import Authorization +from app.deps.authorization_deps import Authorization, CheckStatus from app.keycloak_auth import ( get_token, get_user, @@ -46,6 +46,7 @@ DatasetOut, DatasetPatch, DatasetDBViewList, + DatasetStatus, ) from app.models.files import FileOut, FileDB, FileDBViewList from app.models.folders import FolderOut, FolderIn, FolderDB, FolderDBViewList @@ -223,6 +224,7 @@ async def get_datasets( Or( DatasetDBViewList.creator.email == user_id, DatasetDBViewList.auth.user_ids == user_id, + DatasetDBViewList.status == DatasetStatus.AUTHENTICATED.name, ), sort=(-DatasetDBViewList.created), skip=skip, @@ -235,6 +237,7 @@ async def get_datasets( @router.get("/{dataset_id}", response_model=DatasetOut) async def get_dataset( dataset_id: str, + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), allow: bool = Depends(Authorization("viewer")), ): if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: @@ -246,18 +249,24 @@ async def get_dataset( async def get_dataset_files( dataset_id: str, folder_id: Optional[str] = None, - user_id=Depends(get_user), + authenticated: bool = Depends(CheckStatus("AUTHENTICATED")), allow: bool = Depends(Authorization("viewer")), + user_id=Depends(get_user), skip: int = 0, limit: int = 10, ): - query = [ - FileDBViewList.dataset_id == ObjectId(dataset_id), - Or( - FileDBViewList.creator.email == user_id, - FileDBViewList.auth.user_ids == user_id, - ), - ] + if authenticated: + query = [ + FileDBViewList.dataset_id == ObjectId(dataset_id), + ] + else: + query = [ + FileDBViewList.dataset_id == ObjectId(dataset_id), + Or( + FileDBViewList.creator.email == user_id, + FileDBViewList.auth.user_ids == user_id, + ), + ] if folder_id is not None: query.append(FileDBViewList.folder_id == ObjectId(folder_id)) files = await FileDBViewList.find(*query).skip(skip).limit(limit).to_list() @@ -298,6 +307,8 @@ async def patch_dataset( dataset.name = dataset_info.name if dataset_info.description is not None: dataset.description = dataset_info.description + if dataset_info.status is not None: + dataset.status = dataset_info.status dataset.modified = datetime.datetime.utcnow() await dataset.save() @@ -363,17 +374,23 @@ async def get_dataset_folders( parent_folder: Optional[str] = None, user_id=Depends(get_user), allow: bool = Depends(Authorization("viewer")), + authenticated: bool = Depends(CheckStatus("authenticated")), skip: int = 0, limit: int = 10, ): if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None: - query = [ - FolderDBViewList.dataset_id == ObjectId(dataset_id), - Or( - FolderDBViewList.creator.email == user_id, - FolderDBViewList.auth.user_ids == user_id, - ), - ] + if authenticated: + query = [ + FolderDBViewList.dataset_id == ObjectId(dataset_id), + ] + else: + query = [ + FolderDBViewList.dataset_id == ObjectId(dataset_id), + Or( + FolderDBViewList.creator.email == user_id, + FolderDBViewList.auth.user_ids == user_id, + ), + ] if parent_folder is not None: query.append(FolderDBViewList.parent_folder == ObjectId(parent_folder)) else: diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 000000000..ad0a0ec6b --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/frontend/src/components/datasets/Dataset.tsx b/frontend/src/components/datasets/Dataset.tsx index e43c88bcb..f65c23366 100644 --- a/frontend/src/components/datasets/Dataset.tsx +++ b/frontend/src/components/datasets/Dataset.tsx @@ -308,14 +308,17 @@ export const Dataset = (): JSX.Element => { {...a11yProps(3)} disabled={false} /> - } - iconPosition="start" - sx={TabStyle} - label="Extract" - {...a11yProps(4)} - disabled={false} - /> + {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? + } + iconPosition="start" + sx={TabStyle} + label="Extract" + {...a11yProps(4)} + disabled={false} + /> : + <> + } } iconPosition="start" @@ -324,14 +327,17 @@ export const Dataset = (): JSX.Element => { {...a11yProps(5)} disabled={false} /> - } - iconPosition="start" - sx={TabStyle} - label="Sharing" - {...a11yProps(6)} - disabled={false} - /> + {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? + } + iconPosition="start" + sx={TabStyle} + label="Sharing" + {...a11yProps(6)} + disabled={false} + /> : + <> + } {folderId !== null ? ( @@ -347,7 +353,7 @@ export const Dataset = (): JSX.Element => { - {enableAddMetadata ? ( + {enableAddMetadata && datasetRole.role !== undefined && datasetRole.role !== "viewer" ? ( <> { resourceId={datasetId} /> - + : + <> + } )} @@ -400,15 +409,21 @@ export const Dataset = (): JSX.Element => { resourceId={datasetId} /> - - - + {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? + + + : + <> + } - - - + {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? + + + + : <> + } (); + const [showSuccessAlert, setShowSuccessAlert] = useState(false); + const editDataset = (datasetId: string | undefined, formData: DatasetIn) => + dispatch(updateDataset(datasetId, formData)); + const about = useSelector((state: RootState) => state.dataset.about); + const [datasetStatus, setDatasetStatus] = useState(about["status"]); + const [loading, setLoading] = useState(false); + + const onSetStatus = () => { + setLoading(true); + editDataset(datasetId, { status: datasetStatus }); + setLoading(false); + handleClose(true); + }; + + return ( + + + + + Change Dataset Status '{datasetName}' + + + + Change the status of your dataset +
+ + Status + + +
+ +
+ { + setShowSuccessAlert(false); + }} + > + + + } + sx={{ mb: 2 }} + > + Successfully added role! + +
+
+ + + + +
+
+
+ ); +} diff --git a/frontend/src/components/datasets/OtherMenu.tsx b/frontend/src/components/datasets/OtherMenu.tsx index 62145d6bd..ce3da6c07 100644 --- a/frontend/src/components/datasets/OtherMenu.tsx +++ b/frontend/src/components/datasets/OtherMenu.tsx @@ -17,9 +17,11 @@ import { MoreHoriz } from "@material-ui/icons"; import DeleteIcon from "@mui/icons-material/Delete"; import EditNameModal from "./EditNameModal"; import EditDescriptionModal from "./EditDescriptionModal"; +import EditStatusModal from "./EditStatusModal"; import { DriveFileRenameOutline } from "@mui/icons-material"; import { AuthWrapper } from "../auth/AuthWrapper"; import { RootState } from "../../types/data"; +import ShareIcon from "@mui/icons-material/Share"; type ActionsMenuProps = { datasetId: string; @@ -52,6 +54,7 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { const [description, setDescription] = React.useState(false); const [deleteDatasetConfirmOpen, setDeleteDatasetConfirmOpen] = useState(false); + const [editStatusPaneOpen, setEditStatusPaneOpen] = useState(false); const handleSetRename = () => { setRename(false); @@ -60,6 +63,10 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { setDescription(false); }; + const handleEditStatusClose = () => { + setEditStatusPaneOpen(false); + }; + // delete dataset const deleteSelectedDataset = () => { if (datasetId) { @@ -92,6 +99,10 @@ export const OtherMenu = (props: ActionsMenuProps): JSX.Element => { handleClose={handleSetDescription} open={description} /> + { Update Description + { + handleOptionClose(); + setEditStatusPaneOpen(true); + } + }> + + + + Change Status + { const metadataDefinitionList = useSelector( (state: RootState) => state.metadata.metadataDefinitionList ); + const datasetRole = useSelector( + (state: RootState) => state.dataset.datasetRole + ); useEffect(() => { getMetadatDefinitions(null, 0, 100); @@ -54,6 +57,7 @@ export const CreateMetadata = (props: MetadataType) => { setMetadata: setMetadata, initialReadOnly: false, isRequired: field.required, + datasetRole: datasetRole, key: idxx, } ); diff --git a/frontend/src/components/metadata/DisplayMetadata.tsx b/frontend/src/components/metadata/DisplayMetadata.tsx index 5e100b77e..a223344a3 100644 --- a/frontend/src/components/metadata/DisplayMetadata.tsx +++ b/frontend/src/components/metadata/DisplayMetadata.tsx @@ -30,7 +30,10 @@ export const DisplayMetadata = (props: MetadataType) => { const listFileMetadata = (fileId: string | undefined) => dispatch(fetchFileMetadata(fileId)); const datasetMetadataList = useSelector((state: RootState) => state.metadata.datasetMetadataList); const fileMetadataList = useSelector((state: RootState) => state.metadata.fileMetadataList); - + const datasetRole = useSelector( + (state: RootState) => state.dataset.datasetRole + ); + console.log(updateMetadata, 'updateMetadataDisplay'); useEffect(() => { getMetadatDefinitions(null, 0, 100); }, []); @@ -75,7 +78,8 @@ export const DisplayMetadata = (props: MetadataType) => { content: metadata.content ?? null, metadataId: metadata.id ?? null, isRequired: field.required, - key:idxx + key:idxx, + datasetRole: datasetRole } ); }) @@ -83,11 +87,14 @@ export const DisplayMetadata = (props: MetadataType) => { - + /> : + <> + }
diff --git a/frontend/src/components/metadata/EditMetadata.tsx b/frontend/src/components/metadata/EditMetadata.tsx index 015c073fe..c9377cd63 100644 --- a/frontend/src/components/metadata/EditMetadata.tsx +++ b/frontend/src/components/metadata/EditMetadata.tsx @@ -27,6 +27,10 @@ export const EditMetadata = (props: MetadataType) => { const listFileMetadata = (fileId: string | undefined) => dispatch(fetchFileMetadata(fileId)); const datasetMetadataList = useSelector((state: RootState) => state.metadata.datasetMetadataList); const fileMetadataList = useSelector((state: RootState) => state.metadata.fileMetadataList); + const datasetRole = useSelector( + (state: RootState) => state.dataset.datasetRole + ); + useEffect(() => { getMetadatDefinitions(null, 0, 100); @@ -81,6 +85,7 @@ export const EditMetadata = (props: MetadataType) => { setMetadata: setMetadata, initialReadOnly: false, isRequired: field.required, + datasetRole: datasetRole } ); }) diff --git a/frontend/src/components/metadata/widgets/MetadataDateTimePicker.tsx b/frontend/src/components/metadata/widgets/MetadataDateTimePicker.tsx index 599f3a012..7d9c31a75 100644 --- a/frontend/src/components/metadata/widgets/MetadataDateTimePicker.tsx +++ b/frontend/src/components/metadata/widgets/MetadataDateTimePicker.tsx @@ -8,13 +8,14 @@ import {Grid} from "@mui/material"; export const MetadataDateTimePicker = (props) => { - const {widgetName, fieldName, metadataId, content, setMetadata, initialReadOnly, resourceId, updateMetadata} = props; + const {widgetName, fieldName, metadataId, content, setMetadata, initialReadOnly, resourceId, updateMetadata, datasetRole} = props; const [localContent, setLocalContent] = useState(content && content[fieldName] ? content: {}); const [readOnly, setReadOnly] = useState(initialReadOnly); const [inputChanged, setInputChanged] = useState(false); - + console.log(updateMetadata, 'datetime'); + console.log(datasetRole, 'datasetRole'); const handleChange = (newValue:Date) => { setInputChanged(true); @@ -55,10 +56,14 @@ export const MetadataDateTimePicker = (props) => { - + : + <> + } diff --git a/frontend/src/components/metadata/widgets/MetadataSelect.tsx b/frontend/src/components/metadata/widgets/MetadataSelect.tsx index 27d07881d..510d37ebf 100644 --- a/frontend/src/components/metadata/widgets/MetadataSelect.tsx +++ b/frontend/src/components/metadata/widgets/MetadataSelect.tsx @@ -17,6 +17,7 @@ export const MetadataSelect = (props) => { resourceId, updateMetadata, isRequired, + datasetRole, } = props; const [localContent, setLocalContent] = useState( content && content[fieldName] ? content : {} @@ -74,17 +75,20 @@ export const MetadataSelect = (props) => { - + {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? + : + <> + } diff --git a/frontend/src/components/metadata/widgets/MetadataTextField.tsx b/frontend/src/components/metadata/widgets/MetadataTextField.tsx index 2e0021adc..2b89c0917 100644 --- a/frontend/src/components/metadata/widgets/MetadataTextField.tsx +++ b/frontend/src/components/metadata/widgets/MetadataTextField.tsx @@ -4,10 +4,9 @@ import { MetadataEditButton } from "./MetadataEditButton"; import {Grid} from "@mui/material"; export const MetadataTextField = (props) => { - const {widgetName, fieldName, content, setMetadata, metadataId, updateMetadata, resourceId, initialReadOnly, isRequired} = props; + const {widgetName, fieldName, content, setMetadata, metadataId, updateMetadata, resourceId, initialReadOnly, isRequired, datasetRole} = props; const [localContent, setLocalContent] = useState(content && content[fieldName] ? content: {}); const [readOnly, setReadOnly] = useState(initialReadOnly); - const [inputChanged, setInputChanged] = useState(false); return ( @@ -40,11 +39,14 @@ export const MetadataTextField = (props) => { /> - + {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? + : + <> + } ); diff --git a/frontend/src/openapi/v2/models/DatasetPatch.ts b/frontend/src/openapi/v2/models/DatasetPatch.ts index 5e1c3a361..b6047ecb8 100644 --- a/frontend/src/openapi/v2/models/DatasetPatch.ts +++ b/frontend/src/openapi/v2/models/DatasetPatch.ts @@ -5,4 +5,5 @@ export type DatasetPatch = { name?: string; description?: string; + status?: string; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..cc7c1c772 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "clowder2", + "lockfileVersion": 2, + "requires": true, + "packages": {} +}