diff --git a/catalog/app/containers/Bucket/Dir.tsx b/catalog/app/containers/Bucket/Dir.tsx index dc8ee877f9f..450ff1e013b 100644 --- a/catalog/app/containers/Bucket/Dir.tsx +++ b/catalog/app/containers/Bucket/Dir.tsx @@ -37,17 +37,29 @@ interface DirectoryMenuProps { } function DirectoryMenu({ bucket, path, className }: DirectoryMenuProps) { + const prefs = BucketPreferences.use() const prompt = FileEditor.useCreateFileInBucket(bucket, path) const menuItems = React.useMemo( - () => [ - { - onClick: prompt.open, - title: 'Create file', - }, - ], - [prompt.open], + () => + BucketPreferences.Result.match( + { + Ok: ({ ui: { actions } }) => { + const menu = [] + if (actions.writeFile) { + menu.push({ + onClick: prompt.open, + title: 'Create file', + }) + } + return menu + }, + _: () => [], + }, + prefs, + ), + [prefs, prompt.open], ) - + if (!menuItems.length) return null return ( <> {prompt.render()} diff --git a/catalog/app/containers/Bucket/File.js b/catalog/app/containers/Bucket/File.js index e36db4c04d4..5f3d6f44001 100644 --- a/catalog/app/containers/Bucket/File.js +++ b/catalog/app/containers/Bucket/File.js @@ -452,13 +452,18 @@ export default function File() { onChange={onViewModeChange} /> )} - {FileEditor.isSupportedFileType(handle.key) && ( - - )} + {BucketPreferences.Result.match({ + Ok: ({ ui: { actions } }) => + actions.writeFile && + FileEditor.isSupportedFileType(handle.key) && ( + + ), + _: () => null, + })} {bookmarks && ( - + {BucketPreferences.Result.match({ + Ok: ({ ui: { actions } }) => + actions.writeFile && ( + + ), + _: () => null, + })} ), diff --git a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx index 8da2e1357f9..f4fb4c57107 100644 --- a/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx +++ b/catalog/app/containers/Bucket/PackageTree/RevisionMenu.tsx @@ -26,7 +26,7 @@ export default function RevisionMenu({ { Ok: ({ ui: { actions } }) => { const menu = [] - if (actions.revisePackage) { + if (actions.writeFile && actions.revisePackage) { menu.push({ onClick: onCreateFile, title: 'Create file', diff --git a/catalog/app/utils/BucketPreferences/BucketPreferences.spec.ts b/catalog/app/utils/BucketPreferences/BucketPreferences.spec.ts index 49b4f93693c..8efd7c64d47 100644 --- a/catalog/app/utils/BucketPreferences/BucketPreferences.spec.ts +++ b/catalog/app/utils/BucketPreferences/BucketPreferences.spec.ts @@ -8,8 +8,11 @@ const expectedDefaults = { copyPackage: true, createPackage: true, deleteRevision: false, + openInDesktop: false, revisePackage: true, + writeFile: true, }, + athena: {}, blocks: { analytics: true, browser: true, @@ -22,6 +25,12 @@ const expectedDefaults = { expanded: false, }, }, + gallery: { + files: true, + overview: true, + packages: true, + summarize: true, + }, }, nav: { files: true, @@ -54,7 +63,7 @@ describe('utils/BucketPreferences', () => { actions: copyPackage: False ` - expect(parse(config).ui.actions).toMatchObject({ + expect(parse(config).ui.actions).toEqual({ ...expectedDefaults.ui.actions, copyPackage: false, }) @@ -66,7 +75,7 @@ describe('utils/BucketPreferences', () => { blocks: analytics: False ` - expect(parse(config).ui.blocks).toMatchObject({ + expect(parse(config).ui.blocks).toEqual({ ...expectedDefaults.ui.blocks, analytics: false, }) @@ -78,19 +87,23 @@ describe('utils/BucketPreferences', () => { nav: queries: False ` - expect(parse(config).ui.nav).toMatchObject({ + expect(parse(config).ui.nav).toEqual({ ...expectedDefaults.ui.nav, queries: false, }) }) - it('Additional config structures returns defaults', () => { + it('Additional config structures returns defaults and those additonal fields', () => { const config = dedent` ui: blocks: queries: QUERY ` expect(parse(config)).toMatchObject(expectedDefaults) + expect(parse(config).ui.blocks).toEqual({ + ...expectedDefaults.ui.blocks, + queries: 'QUERY', + }) }) it('Invalid config values throws error', () => { @@ -101,6 +114,14 @@ describe('utils/BucketPreferences', () => { ` expect(() => parse(config)).toThrowError() }) + + it('Actions = false disables all actions', () => { + const config = dedent` + ui: + actions: False + ` + expect(parse(config).ui.actions).toMatchSnapshot() + }) }) describe('extendDefaults', () => { @@ -116,7 +137,7 @@ describe('utils/BucketPreferences', () => { }, }, } - expect(extendDefaults(config).ui.actions).toMatchObject({ + expect(extendDefaults(config).ui.actions).toEqual({ ...expectedDefaults.ui.actions, deleteRevision: true, }) @@ -130,7 +151,7 @@ describe('utils/BucketPreferences', () => { }, }, } - expect(extendDefaults(config).ui.blocks).toMatchObject({ + expect(extendDefaults(config).ui.blocks).toEqual({ ...expectedDefaults.ui.blocks, browser: false, }) @@ -144,7 +165,7 @@ describe('utils/BucketPreferences', () => { }, }, } - expect(extendDefaults(config).ui.nav).toMatchObject({ + expect(extendDefaults(config).ui.nav).toEqual({ ...expectedDefaults.ui.nav, files: false, }) diff --git a/catalog/app/utils/BucketPreferences/BucketPreferences.ts b/catalog/app/utils/BucketPreferences/BucketPreferences.ts index d2d5c53c170..ebd80fd2610 100644 --- a/catalog/app/utils/BucketPreferences/BucketPreferences.ts +++ b/catalog/app/utils/BucketPreferences/BucketPreferences.ts @@ -9,7 +9,12 @@ import * as tagged from 'utils/taggedV2' import * as YAML from 'utils/yaml' export type ActionPreferences = Record< - 'copyPackage' | 'createPackage' | 'deleteRevision' | 'openInDesktop' | 'revisePackage', + | 'copyPackage' + | 'createPackage' + | 'deleteRevision' + | 'openInDesktop' + | 'revisePackage' + | 'writeFile', boolean > @@ -79,7 +84,7 @@ export interface AthenaPreferences { } interface UiPreferencesInput { - actions?: Partial + actions?: Partial | false athena?: AthenaPreferences blocks?: Partial defaultSourceBucket?: DefaultSourceBucketInput @@ -135,6 +140,7 @@ const defaultPreferences: BucketPreferences = { deleteRevision: false, openInDesktop: false, revisePackage: true, + writeFile: true, }, athena: {}, blocks: { @@ -175,6 +181,17 @@ function validate(data: unknown): asserts data is BucketPreferencesInput { if (errors.length) throw new bucketErrors.BucketPreferencesInvalid({ errors }) } +function parseActions(actions?: Partial | false): ActionPreferences { + if (actions === false) { + return R.map(R.F, defaultPreferences.ui.actions) + } + + return { + ...defaultPreferences.ui.actions, + ...actions, + } +} + function parseAthena(athena?: AthenaPreferencesInput): AthenaPreferences { const { defaultWorkflow, ...rest } = { ...defaultPreferences.ui.athena, ...athena } return { @@ -266,6 +283,7 @@ export function extendDefaults(data: BucketPreferencesInput): BucketPreferences return { ui: { ...R.mergeDeepRight(defaultPreferences.ui, data?.ui || {}), + actions: parseActions(data?.ui?.actions), athena: parseAthena(data?.ui?.athena), blocks: parseBlocks(data?.ui?.blocks), packageDescription: parsePackages( diff --git a/catalog/app/utils/BucketPreferences/__snapshots__/BucketPreferences.spec.ts.snap b/catalog/app/utils/BucketPreferences/__snapshots__/BucketPreferences.spec.ts.snap new file mode 100644 index 00000000000..ad7b925aabd --- /dev/null +++ b/catalog/app/utils/BucketPreferences/__snapshots__/BucketPreferences.spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`utils/BucketPreferences parse Actions = false disables all actions 1`] = ` +{ + "copyPackage": false, + "createPackage": false, + "deleteRevision": false, + "openInDesktop": false, + "revisePackage": false, + "writeFile": false, +} +`; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1a777e2dd3d..cf9c533219e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,7 +20,8 @@ Entries inside each section should be ordered by type: ## CLI ## Catalog, Lambdas -- [Added] Support multiple roles per user ([#3982](https://github.com/quiltdata/quilt/pull/3982)) +* [Added] Support multiple roles per user ([#3982](https://github.com/quiltdata/quilt/pull/3982)) +* [Added] Add `ui.actions = False` and `ui.actions.writeFile` for configuring visibility of buttons ([#4001](https://github.com/quiltdata/quilt/pull/4001)) # 6.0.0a4 - 2024-06-18 ## Python API diff --git a/docs/Catalog/Preferences.md b/docs/Catalog/Preferences.md index 6172427bf86..f29085adf79 100644 --- a/docs/Catalog/Preferences.md +++ b/docs/Catalog/Preferences.md @@ -21,6 +21,7 @@ ui: createPackage: True deleteRevision: False revisePackage: True + writeFile: True blocks: analytics: True browser: True @@ -46,11 +47,14 @@ ui: * `ui.nav.files: False` - hide Files tab * `ui.nav.packages: False` - hide Packages tab * `ui.nav.queries: False` - hide Queries tab +* `ui.actions: False` - hide all buttons used to create and edit packages and files +(make the catalog "read-only") * `ui.actions.copyPackage: False` - hide buttons to push packages across buckets * `ui.actions.createPackage: False` - hide buttons to create packages via drag-and-drop or from folders in S3 * `ui.actions.deleteRevision: True` - show buttons to delete package revision * `ui.actions.revisePackage: False` - hide the button to revise packages +* `ui.actions.writeFile: False` - hide buttons to create or edit files * `ui.blocks.analytics: False` - hide Analytics block on file page * `ui.blocks.browser: False` - hide files browser on both Bucket and Packages tab * `ui.blocks.code: False` - hide Code block with quilt3 code boilerplate diff --git a/shared/schemas/bucketConfig.yml.json b/shared/schemas/bucketConfig.yml.json index cb211114f85..310b0a9b562 100644 --- a/shared/schemas/bucketConfig.yml.json +++ b/shared/schemas/bucketConfig.yml.json @@ -36,34 +36,51 @@ } }, "actions": { - "type": "object", - "description": "Hide and show action buttons", - "properties": { - "createPackage": { - "default": true, - "description": "Hides buttons triggering Create Package dialog, both creating package from scratch and from directory", - "type": "boolean", - "examples": [true, false] - }, - "deleteRevision": { - "default": true, - "description": "Hides buttons triggering Delete Package Revision dialog", - "type": "boolean", - "examples": [true, false] - }, - "revisePackage": { - "default": true, - "description": "Hides button triggering Revise Package dialog", - "type": "boolean", - "examples": [true, false] - }, - "copyPackage": { - "default": true, - "description": "Hides button triggering Push to Bucket dialog", - "type": "boolean", - "examples": [true, false] + "default": { + "createPackage": true, + "deleteRevision": true, + "revisePackage": true, + "copyPackage": true + }, + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "description": "Hide and show action buttons", + "properties": { + "copyPackage": { + "description": "Hides button triggering Push to Bucket dialog", + "type": "boolean", + "examples": [true, false] + }, + "createPackage": { + "description": "Hides buttons triggering Create Package dialog, both creating package from scratch and from directory", + "type": "boolean", + "examples": [true, false] + }, + "deleteRevision": { + "description": "Hides buttons triggering Delete Package Revision dialog", + "type": "boolean", + "examples": [true, false] + }, + "openInDesktop": { + "description": "Hides button to open current package in Quilt Desktop", + "type": "boolean", + "examples": [true, false] + }, + "revisePackage": { + "description": "Hides button triggering Revise Package dialog", + "type": "boolean", + "examples": [true, false] + }, + "writeFile": { + "description": "Hides button to create or edit file", + "type": "boolean", + "examples": [true, false] + } + } } - } + ] }, "blocks": { "description": "Hide and show UI blocks in package detail page",