{preview.header}
@@ -113,6 +121,9 @@ const ArtifactsPreviewView = ({ className, preview, setShowErrorBody, showErrorB
{preview?.type === 'text' && (
{preview?.data.content}
)}
+ {preview?.type === 'code' && (
+
+ )}
{preview?.type === 'html' && (
)}
@@ -145,6 +156,22 @@ const ArtifactsPreviewView = ({ className, preview, setShowErrorBody, showErrorB
alt="preview"
/>
)}
+ {preview?.type === 'archive' && (
+
+
+ This artifact is an archive (.{preview?.data?.fileFormat})
+
+
+
+ to view contents
+
+
+ )}
{preview?.type === UNKNOWN_STATE && (
{preview?.data?.content ? preview?.data.content : 'No preview'}
@@ -162,7 +189,8 @@ ArtifactsPreviewView.propTypes = {
className: PropTypes.string.isRequired,
preview: PropTypes.object.isRequired,
setShowErrorBody: PropTypes.func.isRequired,
- showErrorBody: PropTypes.bool.isRequired
+ showErrorBody: PropTypes.bool.isRequired,
+ popupButton: PropTypes.element
}
export default ArtifactsPreviewView
diff --git a/src/components/ArtifactsPreview/CodePreview/CodePreview.jsx b/src/components/ArtifactsPreview/CodePreview/CodePreview.jsx
new file mode 100644
index 0000000000..6edbbe1ae0
--- /dev/null
+++ b/src/components/ArtifactsPreview/CodePreview/CodePreview.jsx
@@ -0,0 +1,112 @@
+/*
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+
+In addition, you may not use the software for any purposes that are
+illegal under applicable law, and the grant of the foregoing license
+under the Apache 2.0 license is conditioned upon your compliance with
+such restriction.
+*/
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import Editor from '../../../common/Editor/Editor'
+import { CopyToClipboard } from 'igz-controls/components'
+import { getEditorLanguage } from '../../../common/Editor/editor.util'
+
+import './codePreview.scss'
+
+const CodePreview = ({ preview, popupButton = null }) => {
+ const { content, fileFormat } = preview?.data || {}
+ const { artifact } = preview || {}
+ const code_type = artifact?.code_type || artifact?.spec?.code_type
+ const requirements = artifact?.requirements || artifact?.spec?.requirements
+ const language = artifact?.language || artifact?.spec?.language
+ const artifactName =
+ preview?.header || artifact?.name || artifact?.db_key || preview?.artifactName
+
+ const renderRequirements = requirements => {
+ if (!requirements) return null
+
+ const reqArray = Array.isArray(requirements) ? requirements : [requirements]
+
+ return reqArray.map((req, idx) => (
+
+ {req}
+
+ ))
+ }
+
+ return (
+
+ {artifact.kind && (
+
+
+
+ {artifactName && {artifactName}}
+ {code_type && {code_type}}
+
+ {popupButton}
+
+ {(language || requirements) && (
+
+ {language && (
+
+ Language: {language}
+
+ )}
+ {language && requirements && | }
+ {requirements && (
+
+ Requirements: {renderRequirements(requirements)}
+
+ )}
+
+ )}
+
+ )}
+
+
+ )
+}
+
+CodePreview.propTypes = {
+ preview: PropTypes.shape({
+ header: PropTypes.string,
+ artifact: PropTypes.shape({
+ name: PropTypes.string,
+ db_key: PropTypes.string,
+ kind: PropTypes.string,
+ language: PropTypes.string,
+ spec: PropTypes.shape({
+ language: PropTypes.string,
+ requirements: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)])
+ })
+ }),
+ data: PropTypes.shape({
+ content: PropTypes.string,
+ fileFormat: PropTypes.string
+ }),
+ artifactName: PropTypes.string
+ }).isRequired,
+ popupButton: PropTypes.element
+}
+
+export default CodePreview
diff --git a/src/components/ArtifactsPreview/CodePreview/codePreview.scss b/src/components/ArtifactsPreview/CodePreview/codePreview.scss
new file mode 100644
index 0000000000..0329bb0f72
--- /dev/null
+++ b/src/components/ArtifactsPreview/CodePreview/codePreview.scss
@@ -0,0 +1,135 @@
+@use 'igz-controls/scss/colors';
+
+.code-preview {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+
+ &__header {
+ margin-bottom: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ &-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ gap: 12px;
+
+ &-container {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+ }
+
+ &-details {
+ display: flex;
+ align-items: center;
+ color: colors.$mulledWine;
+ font-size: 14px;
+ line-height: 20px;
+ }
+ }
+
+ &__name {
+ font-weight: 500;
+ font-size: 20px;
+ line-height: 23px;
+ }
+
+ &__kind-chip {
+ background-color: #f2e8f5;
+ color: #9c4b9c;
+ border-radius: 4px;
+ padding: 5px 8px;
+ font-size: 12px;
+ font-weight: 500;
+ }
+
+ &__language {
+ color: colors.$mulledWine;
+
+ &-value {
+ color: colors.$topaz;
+ }
+ }
+
+ &__separator {
+ margin: 0 8px;
+ color: colors.$mulledWine;
+ }
+
+ &__requirements {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: colors.$mulledWine;
+ }
+
+ &__requirement-chip {
+ background-color: colors.$gallery;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-size: 12px;
+ }
+
+ &__body {
+ border: 1px solid colors.$gallery;
+ border-radius: 8px;
+ background-color: #f8f9fa;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ // min-height: 300px;
+ overflow: hidden;
+ margin: 0;
+ }
+
+ &__editor-container {
+ flex: 1;
+ overflow: hidden;
+ padding: 16px 0;
+
+ section {
+ height: 100%;
+ }
+ }
+
+ &__actions {
+ position: absolute;
+ bottom: 16px;
+ right: 16px;
+ z-index: 10;
+ }
+}
+
+.preview-modal {
+ .pop-up-dialog {
+ .code-preview {
+ padding-top: 20px;
+
+ &__editor-container {
+ section {
+ height: 40vh;
+ }
+ }
+ }
+ }
+}
+
+.preview_container-multi {
+ .code-preview {
+ padding-top: 20px;
+
+ &__editor-container {
+ section {
+ height: 40vh;
+ }
+ }
+ }
+}
diff --git a/src/components/ArtifactsPreview/artifactsPreview.scss b/src/components/ArtifactsPreview/artifactsPreview.scss
index 9c8feb71c2..1fbe2d5965 100644
--- a/src/components/ArtifactsPreview/artifactsPreview.scss
+++ b/src/components/ArtifactsPreview/artifactsPreview.scss
@@ -5,15 +5,21 @@
height: auto;
}
+.artifact-preview__wrapper {
+ &-code {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+}
+
.artifact {
&-preview {
display: flex;
flex: 1;
flex-direction: column;
- margin-bottom: 40px;
&__header {
- margin-bottom: 30px;
padding-top: 3px;
padding-left: 45px;
@@ -41,6 +47,19 @@
overflow-wrap: break-word;
}
+ &__code {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+
+ &-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 8px;
+ }
+ }
+
.json-content {
white-space: pre-wrap;
}
@@ -78,6 +97,36 @@
width: 100%;
}
+ &__archive {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ min-height: 200px;
+
+ &-text {
+ margin-bottom: 5px;
+ font-weight: 700;
+ font-size: 16px;
+ }
+
+ &-download {
+ display: flex;
+ font-weight: 700;
+ font-size: 16px;
+
+ .download {
+ color: colors.$cornflowerBlue;
+ text-decoration: underline;
+
+ &__label {
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+
iframe {
min-height: 560px;
}
diff --git a/src/components/Details/DetailsHeader/DetailsHeader.jsx b/src/components/Details/DetailsHeader/DetailsHeader.jsx
index e65244c818..a992aff7ec 100644
--- a/src/components/Details/DetailsHeader/DetailsHeader.jsx
+++ b/src/components/Details/DetailsHeader/DetailsHeader.jsx
@@ -29,6 +29,7 @@ import { Tooltip, TextTooltipTemplate, RoundedIcon } from 'igz-controls/componen
import { ACTIONS_MENU } from 'igz-controls/types'
import {
ABORTED_STATE,
+ ARTIFACT_PAGES,
DETAILS_ARTIFACTS_TAB,
DETAILS_LOGS_TAB,
JOBS_PAGE
@@ -42,6 +43,8 @@ import { useDetailsHeader } from 'igz-controls/hooks/useDetailsHeader.hook'
import Back from 'igz-controls/images/back-arrow.svg?react'
import InfoIcon from 'igz-controls/images/info-fill.svg?react'
+import './DetailsHeader.scss'
+
const DetailsHeader = ({
actionsMenu,
applyChanges,
@@ -82,6 +85,10 @@ const DetailsHeader = ({
const detailsStore = useSelector(store => store.detailsStore)
const dispatch = useDispatch()
+ const isArtifactPage = useMemo(() => {
+ return ARTIFACT_PAGES.includes(pageData.page)
+ }, [pageData.page])
+
const errorMessage = useMemo(
() =>
selectedItem.reason
@@ -130,6 +137,11 @@ const DetailsHeader = ({
}>
{selectedItem.name || selectedItem.db_key}
+ {isArtifactPage && selectedItem.kind && (
+ }>
+ {selectedItem.kind}
+
+ )}
>
)
}, [
@@ -138,8 +150,10 @@ const DetailsHeader = ({
isDetailsPopUp,
getCloseDetailsLink,
selectedItem.name,
+ selectedItem.db_key,
+ selectedItem.kind,
handleBackClick,
- selectedItem.db_key
+ isArtifactPage
])
const renderStatus = useCallback(() => {
diff --git a/src/components/Details/DetailsHeader/DetailsHeader.scss b/src/components/Details/DetailsHeader/DetailsHeader.scss
new file mode 100644
index 0000000000..3f1b1c1ade
--- /dev/null
+++ b/src/components/Details/DetailsHeader/DetailsHeader.scss
@@ -0,0 +1,15 @@
+@use 'igz-controls/scss/colors';
+
+.item-header__kind-chip {
+ padding: 1px 12px;
+ border: 1px solid colors.$alto;
+ border-radius: 12px;
+ font-size: 11px;
+ margin-left: 8px;
+ color: colors.$doveGray;
+ text-transform: capitalize;
+ font-weight: normal;
+ line-height: 18px;
+ display: inline-block;
+ vertical-align: middle;
+}
diff --git a/src/components/Details/details.util.js b/src/components/Details/details.util.js
index 9ed078f6f1..f7469b815c 100644
--- a/src/components/Details/details.util.js
+++ b/src/components/Details/details.util.js
@@ -238,6 +238,23 @@ export const generateArtifactsContent = (
},
description: {
value: selectedItem.description
+ },
+ language: {
+ value: selectedItem.language
+ },
+ code_type: {
+ fieldData: {
+ name: 'code_type'
+ },
+ editModeEnabled: false,
+ editModeType: 'chips'
+ },
+ requirements: {
+ fieldData: {
+ name: 'requirements'
+ },
+ editModeEnabled: false,
+ editModeType: 'chips'
}
}
}
diff --git a/src/components/DetailsAnalysis/DetailsAnalysis.jsx b/src/components/DetailsAnalysis/DetailsAnalysis.jsx
index 83f91a6eba..43f2444a54 100644
--- a/src/components/DetailsAnalysis/DetailsAnalysis.jsx
+++ b/src/components/DetailsAnalysis/DetailsAnalysis.jsx
@@ -83,7 +83,7 @@ const DetailsAnalysis = ({ artifact }) => {
}, [artifact.analysis, params.projectName])
return (
-
+
)
diff --git a/src/components/DetailsInfo/DetailsInfoView.jsx b/src/components/DetailsInfo/DetailsInfoView.jsx
index c0bcd1d801..fb7c85d9b7 100644
--- a/src/components/DetailsInfo/DetailsInfoView.jsx
+++ b/src/components/DetailsInfo/DetailsInfoView.jsx
@@ -29,16 +29,11 @@ import { Tip } from 'igz-controls/components'
import { isEveryObjectValueEmpty } from '../../utils/isEveryObjectValueEmpty'
import {
ALERTS_PAGE,
- ARTIFACTS_PAGE,
- DATASETS_PAGE,
- DOCUMENTS_PAGE,
+ ARTIFACT_PAGES,
FEATURE_SETS_TAB,
FEATURE_STORE_PAGE,
- FILES_PAGE,
FUNCTIONS_PAGE,
- JOBS_PAGE,
- LLM_PROMPTS_PAGE,
- MODELS_PAGE
+ JOBS_PAGE
} from '../../constants'
import { getChipOptions } from 'igz-controls/utils/chips.util'
@@ -86,15 +81,10 @@ const DetailsInfoView = React.forwardRef(
!isEveryObjectValueEmpty(infoContent) && (
<>
- {(pageData.page === ALERTS_PAGE ||
- pageData.page === ARTIFACTS_PAGE ||
- pageData.page === DATASETS_PAGE ||
- pageData.page === FILES_PAGE ||
+ {(ARTIFACT_PAGES.includes(pageData.page) ||
+ pageData.page === ALERTS_PAGE ||
pageData.page === FUNCTIONS_PAGE ||
- pageData.page === MODELS_PAGE ||
- pageData.page === FEATURE_STORE_PAGE ||
- pageData.page === DOCUMENTS_PAGE ||
- pageData.page === LLM_PROMPTS_PAGE) &&
+ pageData.page === FEATURE_STORE_PAGE) &&
params.pageTab !== FEATURE_SETS_TAB &&
General
}
{pageData.details.infoHeaders?.map(header => {
@@ -129,14 +119,9 @@ const DetailsInfoView = React.forwardRef(
: ''
info = infoContent[header.id]?.value
} else if (
+ ARTIFACT_PAGES.includes(pageData.page) ||
pageData.page === ALERTS_PAGE ||
- pageData.page === ARTIFACTS_PAGE ||
- pageData.page === DATASETS_PAGE ||
- pageData.page === FILES_PAGE ||
- pageData.page === MODELS_PAGE ||
- pageData.page === FEATURE_STORE_PAGE ||
- pageData.page === DOCUMENTS_PAGE ||
- pageData.page === LLM_PROMPTS_PAGE
+ pageData.page === FEATURE_STORE_PAGE
) {
if (header.id === 'metrics' || header.id === 'labels') {
chipsData.validationRules = infoContent[header.id]?.validationRules
@@ -144,6 +129,8 @@ const DetailsInfoView = React.forwardRef(
} else if (header.id === 'relations') {
chipsData.chipOptions = getChipOptions(header.id)
chipsData.delimiter =
+ } else if (header.id === 'requirements' || header.id === 'code_type') {
+ chipsData.chipOptions = getChipOptions(header.id)
}
info = !isNil(commonDetailsStore.changes.data[header.id])
diff --git a/src/components/DetailsPreview/DetailsPreview.jsx b/src/components/DetailsPreview/DetailsPreview.jsx
index 708090f1ca..4c7e859a93 100644
--- a/src/components/DetailsPreview/DetailsPreview.jsx
+++ b/src/components/DetailsPreview/DetailsPreview.jsx
@@ -44,13 +44,15 @@ const DetailsPreview = ({ artifact, handlePreview }) => {
const popupButtonIsDisplayed = useMemo(() => {
return (
artifact.target_path &&
+ !preview[0]?.hidePopupBtn &&
(artifact.extra_data?.length > 0 ||
(!preview[0]?.error && !preview.every(item => item.hidden)))
)
}, [artifact.extra_data, artifact.target_path, preview])
const artifactsPreviewClassNames = classnames(
- popupButtonIsDisplayed && 'artifact-preview__with-popout'
+ popupButtonIsDisplayed && 'artifact-preview__with-popout',
+ 'artifact-preview'
)
useEffect(() => {
@@ -88,6 +90,19 @@ const DetailsPreview = ({ artifact, handlePreview }) => {
}
}, [artifact, params.projectName])
+ const popupButton = (
+
+ )
+
return (
@@ -97,22 +112,11 @@ const DetailsPreview = ({ artifact, handlePreview }) => {
This table presents partial data. To view complete data, download it.
)}
- {popupButtonIsDisplayed && (
-
- )}
+ {popupButtonIsDisplayed && popupButton}
{preview[0]?.hidden && artifact.extra_data?.length > 0 ? null : (
-
+
)}
{artifact.extra_data?.length > 0 &&
}
diff --git a/src/components/Files/Files.jsx b/src/components/Files/Files.jsx
index 779104225c..f49a741003 100644
--- a/src/components/Files/Files.jsx
+++ b/src/components/Files/Files.jsx
@@ -43,7 +43,23 @@ const Files = ({ isAllVersions = false }) => {
const generateDetailsFormInitialValues = useCallback(
(selectedFile, internal_labels) => ({
tag: selectedFile.tag ?? '',
- labels: parseChipsData(selectedFile.labels ?? {}, internal_labels)
+ labels: parseChipsData(selectedFile.labels ?? {}, internal_labels),
+ code_type: selectedFile.code_type
+ ? [
+ {
+ id: selectedFile.code_type,
+ key: selectedFile.code_type,
+ isKeyOnly: true
+ }
+ ]
+ : '',
+ requirements: (selectedFile.requirements ?? []).map(requirement => {
+ return {
+ id: requirement,
+ key: requirement,
+ isKeyOnly: true
+ }
+ })
}),
[]
)
diff --git a/src/components/Files/files.util.jsx b/src/components/Files/files.util.jsx
index 7fd979028c..c2ffc8e6a9 100644
--- a/src/components/Files/files.util.jsx
+++ b/src/components/Files/files.util.jsx
@@ -63,7 +63,7 @@ export const detailsMenu = [
}
]
-export const infoHeaders = [
+export const getInfoHeaders = selectedArtifact => [
{
label: 'Hash',
id: 'hash',
@@ -76,16 +76,23 @@ export const infoHeaders = [
{ label: 'Path', id: 'target_path' },
{ label: 'URI', id: 'target_uri' },
{ label: 'Updated', id: 'updated' },
- { label: 'Labels', id: 'labels' }
+ { label: 'Labels', id: 'labels' },
+ ...(selectedArtifact.kind === 'code'
+ ? [
+ { label: 'Code type', id: 'code_type' },
+ { label: 'Language', id: 'language' },
+ { label: 'Requirements', id: 'requirements' }
+ ]
+ : [])
]
-export const generatePageData = (viewMode, isDetailsPopUp = false) => {
+export const generatePageData = (viewMode, isDetailsPopUp = false, selectedArtifact) => {
return {
page: FILES_PAGE,
details: {
type: FILES_PAGE,
menu: detailsMenu,
- infoHeaders,
+ infoHeaders: getInfoHeaders(selectedArtifact),
hideBackBtn: viewMode === FULL_VIEW_MODE && !isDetailsPopUp,
hideCloseBtn: viewMode === FULL_VIEW_MODE && !isDetailsPopUp,
withToggleViewBtn: true
diff --git a/src/constants.js b/src/constants.js
index 76adcbe734..c70255b3ef 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -139,6 +139,15 @@ export const LLM_PROMPTS_PAGE = 'llm-prompts'
export const PROMPT_TAB = 'prompt'
export const ARGUMENTS_TAB = 'arguments'
+export const ARTIFACT_PAGES = [
+ ARTIFACTS_PAGE,
+ DATASETS_PAGE,
+ DOCUMENTS_PAGE,
+ FILES_PAGE,
+ LLM_PROMPTS_PAGE,
+ MODELS_PAGE
+]
+
export const PROJECT_MONITOR = 'monitor'
export const PROJECT_QUICK_ACTIONS_PAGE = 'quick-actions'
diff --git a/src/utils/createArtifactPreviewContent.js b/src/utils/createArtifactPreviewContent.js
index 404973f5d6..3ffd07ec3c 100644
--- a/src/utils/createArtifactPreviewContent.js
+++ b/src/utils/createArtifactPreviewContent.js
@@ -20,6 +20,7 @@ such restriction.
import { has, isString } from 'lodash'
import { UNKNOWN_STATE } from '../constants'
+import { commonLanguages } from '../common/Editor/editor.util'
const splitStringToArray = str => {
return str.split(/,(?! )/g)
@@ -30,9 +31,12 @@ export const createArtifactPreviewContent = (
fileFormat,
path,
artifactName,
- isPreviewTruncated = false
+ isPreviewTruncated = false,
+ artifact
) => {
- const artifact = {}
+ const previewContent = {
+ artifact
+ }
if (res?.headers['content-type'].includes('text/csv') && isString(res?.data)) {
const data = res.data.split('\n')
@@ -44,59 +48,67 @@ export const createArtifactPreviewContent = (
content.pop()
content = content.map(item => splitStringToArray(item))
- artifact.type = 'table-results'
- artifact.iterationStats = [headers].concat(content)
+ previewContent.type = 'table-results'
+ previewContent.iterationStats = [headers].concat(content)
} else {
let content = data.slice(1)
content = content.map(item => splitStringToArray(item))
content.pop()
- artifact.type = 'table'
- artifact.data = {
+ previewContent.type = 'table'
+ previewContent.data = {
headers: data[0].split(','),
content: content
}
}
} else if (fileFormat === 'yaml' || fileFormat === 'yml') {
- artifact.type = 'yaml'
- artifact.data = {
+ previewContent.type = 'yaml'
+ previewContent.data = {
content: res.data
}
+ } else if (artifact?.kind === 'code' || Object.hasOwn(commonLanguages, fileFormat)) {
+ previewContent.type = 'code'
+ previewContent.hidePopupBtn = true
+ previewContent.data = {
+ content: res.data,
+ fileFormat
+ }
} else if (
res?.headers['content-type'].includes('text/plain') ||
(res?.headers['content-type'].includes('application/octet-stream') && isString(res?.data))
) {
- artifact.type = 'text'
- artifact.data = {
+ previewContent.type = 'text'
+ previewContent.data = {
content: String(res.data)
}
} else if (res?.headers['content-type'].includes('text/html')) {
- artifact.type = 'html'
- artifact.data = {
+ previewContent.type = 'html'
+ previewContent.data = {
content: URL.createObjectURL(res.data)
}
} else if (res?.headers['content-type'].includes('application/json')) {
- artifact.type = 'json'
- artifact.data = {
+ previewContent.type = 'json'
+ previewContent.data = {
content: JSON.stringify(res.data, null, 2)
}
- artifact.hidden = has(res.data, 'listdir')
+ previewContent.hidden = has(res.data, 'listdir')
} else if (res?.headers['content-type'].includes('image')) {
- artifact.type = 'image'
- artifact.data = {
+ previewContent.type = 'image'
+ previewContent.data = {
content: URL.createObjectURL(res.data)
}
} else {
- artifact.type = UNKNOWN_STATE
+ previewContent.type = UNKNOWN_STATE
if (path && artifactName) {
- artifact.data = {
+ previewContent.data = {
content: `Preview is not available for this artifact type. Go to ${path} to retrieve the data, or use mlrun api/sdk project.get_artifact('${artifactName}').to_dataitem().get()`
}
}
}
- if (isPreviewTruncated) artifact.warningMsg = 'The preview is truncated due to the file size'
+ if (isPreviewTruncated)
+ previewContent.warningMsg = 'The preview is truncated due to the file size'
- return artifact
+ return previewContent
}
diff --git a/src/utils/getArtifactPreview.jsx b/src/utils/getArtifactPreview.jsx
index 6cdce1792c..5748253458 100644
--- a/src/utils/getArtifactPreview.jsx
+++ b/src/utils/getArtifactPreview.jsx
@@ -29,6 +29,7 @@ import {
REQUEST_CANCELED,
UNKNOWN_STATE
} from '../constants'
+import { commonLanguages } from '../common/Editor/editor.util'
const fileSizes = {
'100KB': 102400,
@@ -143,6 +144,29 @@ export const fetchArtifactPreviewFromPath = async (
) => {
const user = path.startsWith('/User') && (artifact.user || artifact.producer?.owner)
const fileFormat = path.replace(/.*\./g, '')
+ const isArchive = ['zip', 'gz', 'tgz'].includes(fileFormat)
+
+ if (isArchive) {
+ if (noData) {
+ setNoData(false)
+ }
+
+ return setPreview([
+ {
+ type: 'archive',
+ artifact,
+ data: {
+ path,
+ user: artifact.ui.user,
+ projectName,
+ fileFormat,
+ content: ''
+ },
+ hidePopupBtn: true
+ }
+ ])
+ }
+
let fileSize = artifact.size
try {
@@ -152,7 +176,9 @@ export const fetchArtifactPreviewFromPath = async (
fileSize = fileStats.size
}
- if (supportedFormats.includes(fileFormat)) {
+ const isCode = artifact.kind === 'code' || Object.hasOwn(commonLanguages, fileFormat)
+
+ if (supportedFormats.includes(fileFormat) || isCode) {
if (
fileFormat &&
fileFormat !== path &&
@@ -181,7 +207,8 @@ export const fetchArtifactPreviewFromPath = async (
fileFormat,
artifact.db_key,
{ fileSize, ...artifactLimits },
- signal
+ signal,
+ artifact
)
setPreview([content])
@@ -215,7 +242,8 @@ export const fetchArtifactPreview = async (
fileFormat,
artifactName,
sizeConfigs = {},
- signal
+ signal,
+ artifact
) => {
const defaultSizeLimit = fileSizes['100KB']
const config = {
@@ -256,7 +284,7 @@ export const fetchArtifactPreview = async (
response.data = fullFile
- return createArtifactPreviewContent(response, fileFormat, path, artifactName)
+ return createArtifactPreviewContent(response, fileFormat, path, artifactName, false, artifact)
}
return api.getArtifactPreview(projectName, config).then(response => {
@@ -265,7 +293,8 @@ export const fetchArtifactPreview = async (
fileFormat,
path,
artifactName,
- sizeConfigs.fileSize > defaultSizeLimit
+ sizeConfigs.fileSize > defaultSizeLimit,
+ artifact
)
})
}
diff --git a/tests/mockServer/data/artifacts.json b/tests/mockServer/data/artifacts.json
index 83d6dce255..3d5e1d07dd 100644
--- a/tests/mockServer/data/artifacts.json
+++ b/tests/mockServer/data/artifacts.json
@@ -40374,6 +40374,128 @@
"state": "created"
},
"project": "llmdeploy335"
+ },
+ {
+ "kind": "code",
+ "metadata": {
+ "key": "python_script",
+ "project": "default",
+ "iter": 0,
+ "tree": "mock-tree",
+ "hash": "mock-hash-1",
+ "uid": "mock-uid-1",
+ "updated": "2026-04-23 10:00:00.000000+00:00",
+ "created": "2026-04-23 10:00:00.000000+00:00",
+ "tag": "latest"
+ },
+ "spec": {
+ "target_path": "v3io://artifacts/python_script.py",
+ "size": 100,
+ "db_key": "python_script",
+ "language": "python",
+ "code_type": "function",
+ "requirements": ["pandas", "mlrun"]
+ },
+ "status": {
+ "state": "created"
+ },
+ "project": "default"
+ },
+ {
+ "kind": "code",
+ "metadata": {
+ "key": "javascript_script",
+ "project": "default",
+ "iter": 0,
+ "tree": "mock-tree",
+ "hash": "mock-hash-2",
+ "uid": "mock-uid-2",
+ "updated": "2026-04-23 10:00:00.000000+00:00",
+ "created": "2026-04-23 10:00:00.000000+00:00",
+ "tag": "latest"
+ },
+ "spec": {
+ "target_path": "v3io://artifacts/javascript_script.js",
+ "size": 120,
+ "db_key": "javascript_script",
+ "language": "javascript",
+ "code_type": "function",
+ "requirements": ["lodash"]
+ },
+ "status": {
+ "state": "created"
+ },
+ "project": "default"
+ },
+ {
+ "kind": "code",
+ "metadata": {
+ "key": "shell_script",
+ "project": "default",
+ "iter": 0,
+ "tree": "mock-tree",
+ "hash": "mock-hash-3",
+ "uid": "mock-uid-3",
+ "updated": "2026-04-23 10:00:00.000000+00:00",
+ "created": "2026-04-23 10:00:00.000000+00:00",
+ "tag": "latest"
+ },
+ "spec": {
+ "target_path": "v3io://artifacts/shell_script.sh",
+ "size": 80,
+ "db_key": "shell_script",
+ "code_type": "function"
+ },
+ "status": {
+ "state": "created"
+ },
+ "project": "default"
+ },
+ {
+ "kind": "code",
+ "metadata": {
+ "key": "archive_python",
+ "project": "default",
+ "iter": 0,
+ "tree": "mock-tree",
+ "hash": "mock-hash-4",
+ "uid": "mock-uid-4",
+ "updated": "2026-04-23 10:00:00.000000+00:00",
+ "created": "2026-04-23 10:00:00.000000+00:00",
+ "tag": "latest"
+ },
+ "spec": {
+ "target_path": "v3io://artifacts/archive_python.zip",
+ "size": 1024,
+ "db_key": "archive_python"
+ },
+ "status": {
+ "state": "created"
+ },
+ "project": "default"
+ },
+ {
+ "kind": "code",
+ "metadata": {
+ "key": "archive_js",
+ "project": "default",
+ "iter": 0,
+ "tree": "mock-tree",
+ "hash": "mock-hash-5",
+ "uid": "mock-uid-5",
+ "updated": "2026-04-23 10:00:00.000000+00:00",
+ "created": "2026-04-23 10:00:00.000000+00:00",
+ "tag": "latest"
+ },
+ "spec": {
+ "target_path": "v3io://artifacts/archive_js.zip",
+ "size": 1024,
+ "db_key": "archive_js"
+ },
+ "status": {
+ "state": "created"
+ },
+ "project": "default"
}
]
}
diff --git a/tests/mockServer/data/artifacts/archive_js.zip b/tests/mockServer/data/artifacts/archive_js.zip
new file mode 100644
index 0000000000..708d81ea75
--- /dev/null
+++ b/tests/mockServer/data/artifacts/archive_js.zip
@@ -0,0 +1 @@
+PK...zip_content_placeholder...js...
\ No newline at end of file
diff --git a/tests/mockServer/data/artifacts/archive_python.zip b/tests/mockServer/data/artifacts/archive_python.zip
new file mode 100644
index 0000000000..3c9793e4bd
--- /dev/null
+++ b/tests/mockServer/data/artifacts/archive_python.zip
@@ -0,0 +1 @@
+PK...zip_content_placeholder...python...
\ No newline at end of file
diff --git a/tests/mockServer/data/artifacts/javascript_script.js b/tests/mockServer/data/artifacts/javascript_script.js
new file mode 100644
index 0000000000..52a3808f97
--- /dev/null
+++ b/tests/mockServer/data/artifacts/javascript_script.js
@@ -0,0 +1,153 @@
+/*
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+*/
+
+import { isEmpty, isEqual } from 'lodash'
+
+const CONFIG = {
+ MAX_RETRIES: 3,
+ API_ENDPOINT: 'https://api.mlrun.org',
+ DEFAULT_HEADERS: {
+ 'Content-Type': 'application/json'
+ }
+}
+
+class Logger {
+ constructor() {
+ this.logs = []
+ }
+
+ info(message) {
+ this.logs.push({ level: 'INFO', message, timestamp: new Date().toISOString() })
+ }
+
+ error(message, error) {
+ this.logs.push({ level: 'ERROR', message, error: error.message, timestamp: new Date().toISOString() })
+ }
+
+ getLogs() {
+ return this.logs
+ }
+}
+
+const logger = new Logger()
+
+/**
+ * Base model class
+ */
+class Model {
+ constructor(name, type) {
+ this.name = name
+ this.type = type
+ this.metadata = {}
+ this.status = 'idle'
+ }
+
+ setMetadata(data) {
+ if (!isEmpty(data)) {
+ this.metadata = { ...this.metadata, ...data }
+ }
+ }
+
+ getStatus() {
+ return `Model ${this.name} is currently ${this.status}`
+ }
+}
+
+/**
+ * Specialized CodeModel
+ */
+class CodeModel extends Model {
+ constructor(name, language) {
+ super(name, 'code')
+ this.language = language
+ this.executionHistory = []
+ }
+
+ async run(payload) {
+ this.status = 'running'
+ logger.info(`Starting execution of ${this.name} (${this.language})`)
+
+ try {
+ // Simulate async processing
+ await new Promise(resolve => setTimeout(resolve, 500))
+
+ const result = {
+ id: Math.random().toString(36).substr(2, 9),
+ timestamp: Date.now(),
+ data: payload
+ }
+
+ this.executionHistory.push(result)
+ this.status = 'completed'
+ logger.info(`Execution ${result.id} successful`)
+
+ return result
+ } catch (err) {
+ this.status = 'error'
+ logger.error(`Execution failed for ${this.name}`, err)
+ throw err
+ }
+ }
+
+ getRecentExecutions(limit = 5) {
+ return this.executionHistory.slice(-limit).reverse()
+ }
+}
+
+const main = async () => {
+ const models = [
+ new CodeModel('script_a', 'javascript'),
+ new CodeModel('script_b', 'python'),
+ new Model('simple_model', 'generic')
+ ]
+
+ const tasks = models
+ .filter(m => m.type === 'code')
+ .map(async m => {
+ try {
+ return await m.run({ timestamp: Date.now(), priority: 'high' })
+ } catch {
+ return null
+ }
+ })
+
+ const results = await Promise.all(tasks)
+
+ const summary = {
+ total: models.length,
+ codeModels: models.filter(m => m instanceof CodeModel).length,
+ activeResults: results.filter(r => !isEqual(r, null)).length,
+ systemConfig: { ...CONFIG }
+ }
+
+ return summary
+}
+
+// Execution and more content to reach 100 lines
+// ---------------------------------------------------------
+// This section demonstrates various JS constructions:
+// - Async/Await and Promises
+// - Classes and Inheritance
+// - Template Literals
+// - Spread and Rest operators
+// - Arrow Functions
+// - Array methods (filter, map, slice, reverse)
+// - Object destructuring and shorthand
+// - Modules simulation (imports)
+// - JSDoc comments
+// - Constant objects
+// ---------------------------------------------------------
+
+export { main, CodeModel, Logger }
diff --git a/tests/mockServer/data/artifacts/python_script.py b/tests/mockServer/data/artifacts/python_script.py
new file mode 100644
index 0000000000..4446bcf68e
--- /dev/null
+++ b/tests/mockServer/data/artifacts/python_script.py
@@ -0,0 +1,135 @@
+"""
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+"""
+
+import asyncio
+import functools
+import logging
+from datetime import datetime
+from typing import List, Dict, Optional, Union
+
+# Global constant
+DEFAULT_TIMEOUT = 30
+
+def debug_logger(func):
+ """Decorator to log function execution."""
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ print(f"Calling {func.__name__} with {args} {kwargs}")
+ result = func(*args, **kwargs)
+ print(f"{func.__name__} returned {result}")
+ return result
+ return wrapper
+
+class BaseArtifact:
+ """Base class for all artifacts."""
+ def __init__(self, name: str, metadata: Optional[Dict] = None):
+ self.name = name
+ self.metadata = metadata or {}
+ self.created_at = datetime.now()
+
+ def get_info(self) -> str:
+ return f"Artifact: {self.name}, Created: {self.created_at}"
+
+class CodeArtifact(BaseArtifact):
+ """Specific implementation for code artifacts."""
+ def __init__(self, name: str, language: str, source_code: str):
+ super().__init__(name)
+ self.language = language
+ self.source_code = source_code
+ self.tags: List[str] = []
+
+ @debug_logger
+ def add_tag(self, tag: str) -> None:
+ if tag not in self.tags:
+ self.tags.append(tag)
+
+ async def analyze_code(self) -> Dict[str, Union[int, str]]:
+ """Mock async code analysis."""
+ await asyncio.sleep(0.1)
+ lines = self.source_code.split("\n")
+ return {
+ "num_lines": len(lines),
+ "complexity": "low" if len(lines) < 50 else "medium",
+ "status": "completed"
+ }
+
+def process_artifacts(artifacts: List[BaseArtifact]) -> List[str]:
+ """Process a list of artifacts using list comprehension."""
+ return [art.get_info() for art in artifacts if art.name.startswith("v1")]
+
+async def main():
+ # Example usage of various constructions
+ raw_code = """
+ def hello():
+ return "world"
+ """
+
+ python_art = CodeArtifact("v1_main_script", "python", raw_code)
+ python_art.add_tag("production")
+ python_art.add_tag("mlrun")
+
+ print(f"Artifact tags: {python_art.tags}")
+
+ # Async operations
+ analysis_result = await python_art.analyze_code()
+ print(f"Analysis: {analysis_result}")
+
+ # Data structures and filtering
+ all_arts = [
+ python_art,
+ BaseArtifact("v1_config"),
+ BaseArtifact("experimental_model")
+ ]
+
+ processed = process_artifacts(all_arts)
+ for info in processed:
+ print(info)
+
+ # Exception handling
+ try:
+ raise ValueError("Something went wrong")
+ except ValueError as e:
+ logging.error(f"Caught expected error: {e}")
+ finally:
+ print("Cleanup complete")
+
+ # Dictionary comprehension
+ summary = {art.name: art.created_at.isoformat() for art in all_arts}
+ print(f"Summary: {summary}")
+
+ # Set operations
+ unique_tags = {"dev", "prod", "dev"}
+ print(f"Unique tags: {unique_tags}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+
+# More lines to reach the 100 mark
+# ---------------------------------------------------------
+# Documentation Section
+# ---------------------------------------------------------
+# This script serves as a mock artifact for UI testing.
+# It includes:
+# - Classes and Inheritance
+# - Decorators
+# - Asynchronous programming (async/await)
+# - Type Hinting
+# - List and Dictionary Comprehensions
+# - Exception Handling
+# - Logging
+# - String Formatting (f-strings)
+# - Global and Local variables
+# - Standard Library imports (asyncio, datetime, logging, functools)
+# ---------------------------------------------------------
diff --git a/tests/mockServer/data/artifacts/shell_script.sh b/tests/mockServer/data/artifacts/shell_script.sh
new file mode 100644
index 0000000000..3e5f7cd11c
--- /dev/null
+++ b/tests/mockServer/data/artifacts/shell_script.sh
@@ -0,0 +1,144 @@
+#!/bin/bash
+
+# Copyright 2019 Iguazio Systems Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License") with
+# an addition restriction as set forth herein. You may not use this
+# file except in compliance with the License. You may obtain a copy of
+# the License at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+set -e
+
+# --- Configuration ---
+PROJECT_NAME="mlrun-ui-demo"
+VERSION="1.0.0"
+LOG_FILE="/tmp/setup.log"
+DRY_RUN=false
+
+# --- Colors ---
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m' # No Color
+
+# --- Functions ---
+
+log_info() {
+ local message=$1
+ echo -e "${GREEN}[INFO]${NC} $(date +'%Y-%m-%d %H:%M:%S') - $message" | tee -a "$LOG_FILE"
+}
+
+log_error() {
+ local message=$1
+ echo -e "${RED}[ERROR]${NC} $(date +'%Y-%m-%d %H:%M:%S') - $message" | tee -a "$LOG_FILE" >&2
+}
+
+usage() {
+ echo "Usage: $0 [options]"
+ echo "Options:"
+ echo " -p, --project NAME Set project name (default: $PROJECT_NAME)"
+ echo " -d, --dry-run Perform a dry run"
+ echo " -h, --help Show this help message"
+}
+
+# --- Argument Parsing ---
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -p|--project)
+ PROJECT_NAME="$2"
+ shift 2
+ ;;
+ -d|--dry-run)
+ DRY_RUN=true
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+# --- Main Script ---
+
+log_info "Starting setup for project: $PROJECT_NAME (v$VERSION)"
+
+if [ "$DRY_RUN" = true ]; then
+ log_info "DRY RUN enabled. No changes will be made."
+fi
+
+# Simulate checking dependencies
+dependencies=("curl" "git" "docker" "python3")
+
+for dep in "${dependencies[@]}"; do
+ if command -v "$dep" >/dev/null 2>&1; then
+ log_info "Dependency found: $dep"
+ else
+ log_error "Missing dependency: $dep"
+ # exit 1 (disabled for mock)
+ fi
+done
+
+# Conditionals and Arithmetic
+count=0
+while [ $count -lt 5 ]; do
+ log_info "Processing step $((count + 1))..."
+ ((count++))
+ sleep 0.1
+done
+
+# Variable substitution and heredoc
+cat < /tmp/metadata.json
+{
+ "project": "$PROJECT_NAME",
+ "version": "$VERSION",
+ "status": "ready"
+}
+EOF
+
+# Array operations
+log_info "Finalizing configuration..."
+configs=("api" "ui" "db" "store")
+log_info "Configuring sub-systems: ${configs[*]}"
+
+# Case statement example
+os_type=$(uname -s)
+case "$os_type" in
+ Linux*) log_info "Running on Linux" ;;
+ Darwin*) log_info "Running on macOS" ;;
+ *) log_info "Running on unknown OS: $os_type" ;;
+esac
+
+log_info "Setup completed successfully."
+
+# --- Additional lines to reach 100 ---
+# This script is a mock artifact for MLRun UI testing.
+# Features demonstrated:
+# - Shebang line
+# - Comments and Copyright header
+# - Variables and Constants
+# - Functions with local variables
+# - Output redirection and pipes
+# - Command substitution
+# - Arrays
+# - Loops (for, while)
+# - Conditionals (if/then/else)
+# - Case statements
+# - Arithmetic expansion
+# - Here documents (heredoc)
+# - ANSI color escape codes
+# - Argument parsing with shift
+# - Exit codes and set -e
+# ---------------------------------------------------------
+# EOF
diff --git a/tests/mockServer/data/featureSets.json b/tests/mockServer/data/featureSets.json
index 0fde2a6fdd..8ff5efdb62 100644
--- a/tests/mockServer/data/featureSets.json
+++ b/tests/mockServer/data/featureSets.json
@@ -400,7 +400,10 @@
"csv": "v3io://artifacts/csv_mock_data.csv",
"yaml": "v3io://artifacts/yaml_mock_data.yaml",
"text": "v3io://artifacts/text_mock_data.txt",
- "html": "v3io://artifacts/html_mock_data.html"
+ "html": "v3io://artifacts/html_mock_data.html",
+ "code_python": "v3io://artifacts/python_script.py",
+ "code_js": "v3io://artifacts/javascript_script.js",
+ "code_archive": "v3io://artifacts/archive_python.zip"
},
"output_path": "v3io:///projects/{{run.project}}/artifacts"
},
diff --git a/tests/mockServer/mock.js b/tests/mockServer/mock.js
index 13f5109b70..2222ed5086 100644
--- a/tests/mockServer/mock.js
+++ b/tests/mockServer/mock.js
@@ -215,7 +215,7 @@ const artifactsCategories = {
document: ['document'],
model: ['model'],
'llm-prompt': ['llm-prompt'],
- other: ['', 'table', 'link', 'plot', 'chart', 'plotly', 'artifact']
+ other: ['', 'table', 'link', 'plot', 'chart', 'plotly', 'artifact', 'code']
}
// Support functions