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",